<?php
namespace GraphQL\Examples\Blog\Type\Scalar;

use GraphQL\Error\Error;
use GraphQL\Language\AST\StringValueNode;
use GraphQL\Type\Definition\CustomScalarType;
use GraphQL\Utils\Utils;

class EmailType
{
    public static function create()
    {
        return new CustomScalarType([
            'name' => 'Email',
            'serialize' => [__CLASS__, 'serialize'],
            'parseValue' => [__CLASS__, 'parseValue'],
            'parseLiteral' => [__CLASS__, 'parseLiteral'],
        ]);
    }

    /**
     * Serializes an internal value to include in a response.
     *
     * @param string $value
     * @return string
     */
    public static function serialize($value)
    {
        // Assuming internal representation of email is always correct:
        return $value;

        // If it might be incorrect and you want to make sure that only correct values are included in response -
        // use following line instead:
        // return $this->parseValue($value);
    }

    /**
     * Parses an externally provided value (query variable) to use as an input
     *
     * @param mixed $value
     * @return mixed
     */
    public static function parseValue($value)
    {
        if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
            throw new \UnexpectedValueException("Cannot represent value as email: " . Utils::printSafe($value));
        }
        return $value;
    }

    /**
     * Parses an externally provided literal value (hardcoded in GraphQL query) to use as an input
     *
     * @param \GraphQL\Language\AST\Node $valueNode
     * @return string
     * @throws Error
     */
    public static function parseLiteral($valueNode)
    {
        // Note: throwing GraphQL\Error\Error vs \UnexpectedValueException to benefit from GraphQL
        // error location in query:
        if (!$valueNode instanceof StringValueNode) {
            throw new Error('Query error: Can only parse strings got: ' . $valueNode->kind, [$valueNode]);
        }
        if (!filter_var($valueNode->value, FILTER_VALIDATE_EMAIL)) {
            throw new Error("Not a valid email", [$valueNode]);
        }
        return $valueNode->value;
    }
}


<?php

declare(strict_types=1);

namespace GraphQL\Language;

use GraphQL\Utils\Utils;
use function is_string;
use function json_decode;
use function mb_strlen;
use function mb_substr;
use function preg_match_all;
use const PREG_OFFSET_CAPTURE;

class Source
{
    /** @var string */
    public $body;

    /** @var int */
    public $length;

    /** @var string */
    public $name;

    /** @var SourceLocation */
    public $locationOffset;

    /**
     * A representation of source input to GraphQL.
     * `name` and `locationOffset` are optional. They are useful for clients who
     * store GraphQL documents in source files; for example, if the GraphQL input
     * starts at line 40 in a file named Foo.graphql, it might be useful for name to
     * be "Foo.graphql" and location to be `{ line: 40, column: 0 }`.
     * line and column in locationOffset are 1-indexed
     *
     * @param string      $body
     * @param string|null $name
     */
    public function __construct($body, $name = null, ?SourceLocation $location = null)
    {
        Utils::invariant(
            is_string($body),
            'GraphQL query body is expected to be string, but got ' . Utils::getVariableType($body)
        );

        $this->body           = $body;
        $this->length         = mb_strlen($body, 'UTF-8');
        $this->name           = $name ?: 'GraphQL request';
        $this->locationOffset = $location ?: new SourceLocation(1, 1);

        Utils::invariant(
            $this->locationOffset->line > 0,
            'line in locationOffset is 1-indexed and must be positive'
        );
        Utils::invariant(
            $this->locationOffset->column > 0,
            'column in locationOffset is 1-indexed and must be positive'
        );
    }

    /**
     * @param int $position
     *
     * @return SourceLocation
     */
    public function getLocation($position)
    {
        $line   = 1;
        $column = $position + 1;

        $utfChars   = json_decode('"\u2028\u2029"');
        $lineRegexp = '/\r\n|[\n\r' . $utfChars . ']/su';
        $matches    = [];
        preg_match_all($lineRegexp, mb_substr($this->body, 0, $position, 'UTF-8'), $matches, PREG_OFFSET_CAPTURE);

        foreach ($matches[0] as $index => $match) {
            $line += 1;

            $column = $position + 1 - ($match[1] + mb_strlen($match[0], 'UTF-8'));
        }

        return new SourceLocation($line, $column);
    }
}


<?php
namespace Baum\Generators;

use Illuminate\Filesystem\Filesystem;

abstract class Generator {

  /**
   * The filesystem instance.
   *
   * @var \Illuminate\Filesystem\Filesystem
   */
  protected $files = NULL;

  /**
   * Create a new MigrationGenerator instance.
   *
   * @param \Illuminate\Filesystem\Filesysmte $files
   * @return void
   */
  function __construct(Filesystem $files) {
    $this->files = $files;
  }

  /**
   * Get the path to the stubs.
   *
   * @return string
   */
  public function getStubPath() {
    return __DIR__.'/stubs';
  }

  /**
   * Get the filesystem instance.
   *
   * @return \Illuminate\Filesystem\Filesystem
   */
  public function getFilesystem() {
    return $this->files;
  }

  /**
   * Get the given stub by name.
   *
   * @param  string  $table
   * @return void
   */
  protected function getStub($name) {
    if ( stripos($name, '.php') === FALSE )
      $name = $name . '.php';

    return $this->files->get($this->getStubPath() . '/' . $name);
  }

  /**
   * Parse the provided stub and replace via the array given.
   *
   * @param string $stub
   * @param string $replacements
   * @return string
   */
  protected function parseStub($stub, $replacements=array()) {
    $output = $stub;

    foreach ($replacements as $key => $replacement) {
      $search = '{{'.$key.'}}';
      $output = str_replace($search, $replacement, $output);
    }

    return $output;
  }

  /**
   * Inflect to a class name
   *
   * @param string $input
   * @return string
   */
  protected function classify($input) {
    return studly_case(str_singular($input));
  }

  /**
   * Inflect to table name
   *
   * @param string $input
   * @return string
   */
  protected function tableize($input) {
    return snake_case(str_plural($input));
  }
}


<?php
namespace Baum\Providers;

use Baum\Generators\MigrationGenerator;
use Baum\Generators\ModelGenerator;
use Baum\Console\BaumCommand;
use Baum\Console\InstallCommand;
use Illuminate\Support\ServiceProvider;

class BaumServiceProvider extends ServiceProvider {

  /**
   * Baum version
   *
   * @var string
   */
  const VERSION = '1.1.1';

  /**
   * Register the service provider.
   *
   * @return void
   */
  public function register() {
    $this->registerCommands();
  }

  /**
   * Register the commands.
   *
   * @return void
   */
  public function registerCommands() {
    $this->registerBaumCommand();
    $this->registerInstallCommand();

    // Resolve the commands with Artisan by attaching the event listener to Artisan's
    // startup. This allows us to use the commands from our terminal.
    $this->commands('command.baum', 'command.baum.install');
  }

  /**
   * Register the 'baum' command.
   *
   * @return void
   */
  protected function registerBaumCommand() {
    $this->app->singleton('command.baum', function($app) {
      return new BaumCommand;
    });
  }

  /**
   * Register the 'baum:install' command.
   *
   * @return void
   */
  protected function registerInstallCommand() {
    $this->app->singleton('command.baum.install', function($app) {
      $migrator = new MigrationGenerator($app['files']);
      $modeler  = new ModelGenerator($app['files']);

      return new InstallCommand($migrator, $modeler);
    });
  }

  /**
   * Get the services provided by the provider.
   *
   * @return array
   */
  public function provides() {
    return array('command.baum', 'command.baum.install');
  }

}


<?php

namespace Baum\Extensions\Query;

use Illuminate\Database\Query\Builder as BaseBuilder;

class Builder extends BaseBuilder {

  /**
   * Replace the "order by" clause of the current query.
   *
   * @param  string  $column
   * @param  string  $direction
   * @return \Illuminate\Database\Query\Builder|static
   */
  public function reOrderBy($column, $direction = 'asc') {
    $this->orders = null;

    if ( !is_null($column) ) return $this->orderBy($column, $direction);

    return $this;
  }

  /**
   * Execute an aggregate function on the database.
   *
   * @param  string  $function
   * @param  array   $columns
   * @return mixed
   */
  public function aggregate($function, $columns = array('*')) {
    // Postgres doesn't like ORDER BY when there's no GROUP BY clause
    if ( !isset($this->groups) )
      $this->reOrderBy(null);

    return parent::aggregate($function, $columns);
  }

}


<?php
namespace Baum\Extensions\Eloquent;

use Illuminate\Database\Eloquent\Model as BaseModel;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\Eloquent\SoftDeletingScope;
use Baum\Extensions\Query\Builder as QueryBuilder;

abstract class Model extends BaseModel {

  /**
   * Reloads the model from the database.
   *
   * @return \Baum\Node
   *
   * @throws ModelNotFoundException
   */
  public function reload() {
    if ( $this->exists || ($this->areSoftDeletesEnabled() && $this->trashed()) ) {
      $fresh = $this->getFreshInstance();

      if ( is_null($fresh) )
        throw with(new ModelNotFoundException)->setModel(get_called_class());

      $this->setRawAttributes($fresh->getAttributes(), true);

      $this->setRelations($fresh->getRelations());

      $this->exists = $fresh->exists;
    } else {
      // Revert changes if model is not persisted
      $this->attributes = $this->original;
    }

    return $this;
  }

  /**
   * Get the observable event names.
   *
   * @return array
   */
  public function getObservableEvents() {
    return array_merge(array('moving', 'moved'), parent::getObservableEvents());
  }

  /**
   * Register a moving model event with the dispatcher.
   *
   * @param  Closure|string  $callback
   * @return void
   */
  public static function moving($callback, $priority = 0) {
    static::registerModelEvent('moving', $callback, $priority);
  }

  /**
   * Register a moved model event with the dispatcher.
   *
   * @param  Closure|string  $callback
   * @return void
   */
  public static function moved($callback, $priority = 0) {
    static::registerModelEvent('moved', $callback, $priority);
  }

  /**
   * Get a new query builder instance for the connection.
   *
   * @return \Baum\Extensions\Query\Builder
   */
  protected function newBaseQueryBuilder() {
    $conn = $this->getConnection();

    $grammar = $conn->getQueryGrammar();

    return new QueryBuilder($conn, $grammar, $conn->getPostProcessor());
  }

  /**
   * Returns a fresh instance from the database.
   *
   * @return \Baum\Node
   */
  protected function getFreshInstance() {
    if ( $this->areSoftDeletesEnabled() )
      return static::withTrashed()->find($this->getKey());

    return static::find($this->getKey());
  }

  /**
   * Returns wether soft delete functionality is enabled on the model or not.
   *
   * @return boolean
   */
  public function areSoftDeletesEnabled() {
    // To determine if there's a global soft delete scope defined we must
    // first determine if there are any, to workaround a non-existent key error.
    $globalScopes = $this->getGlobalScopes();

    if ( count($globalScopes) === 0 ) return false;

    // Now that we're sure that the calling class has some kind of global scope
    // we check for the SoftDeletingScope existance
    return static::hasGlobalScope(new SoftDeletingScope);
  }

  /**
   * Static method which returns wether soft delete functionality is enabled
   * on the model.
   *
   * @return boolean
   */
  public static function softDeletesEnabled() {
    return with(new static)->areSoftDeletesEnabled();
  }

}


<?php
namespace Baum;

use Baum\Helpers\DatabaseHelper as DB;
use Baum\Node;

class SetBuilder {

  /**
  * Node instance for reference
  *
  * @var \Baum\Node
  */
  protected $node = NULL;

  /**
   * Array which will hold temporary lft, rgt index values for each scope.
   *
   * @var array
   */
  protected $bounds = array();

  /**
   * Create a new \Baum\SetBuilder class instance.
   *
   * @param   \Baum\Node      $node
   * @return  void
   */
  public function __construct($node) {
    $this->node = $node;
  }

  /**
   * Perform the re-calculation of the left and right indexes of the whole
   * nested set tree structure.
   *
   * @param  bool $force
   * @return void
   */
  public function rebuild($force = false) {
    $alreadyValid = forward_static_call(array(get_class($this->node), 'isValidNestedSet'));

    // Do not rebuild a valid Nested Set tree structure
    if ( !$force && $alreadyValid ) return true;

    // Rebuild lefts and rights for each root node and its children (recursively).
    // We go by setting left (and keep track of the current left bound), then
    // search for each children and recursively set the left index (while
    // incrementing that index). When going back up the recursive chain we start
    // setting the right indexes and saving the nodes...
    $self = $this;

    $this->node->getConnection()->transaction(function() use ($self) {
      foreach($self->roots() as $root)
        $self->rebuildBounds($root, 0);
    });
  }

  /**
   * Return all root nodes for the current database table appropiately sorted.
   *
   * @return Illuminate\Database\Eloquent\Collection
   */
  public function roots() {
    return $this->node->newQuery()
      ->whereNull($this->node->getQualifiedParentColumnName())
      ->orderBy($this->node->getQualifiedLeftColumnName())
      ->orderBy($this->node->getQualifiedRightColumnName())
      ->orderBy($this->node->getQualifiedKeyName())
      ->get();
  }

  /**
   * Recompute left and right index bounds for the specified node and its
   * children (recursive call). Fill the depth column too.
   */
  public function rebuildBounds($node, $depth = 0) {
    $k = $this->scopedKey($node);

    $node->setAttribute($node->getLeftColumnName(), $this->getNextBound($k));
    $node->setAttribute($node->getDepthColumnName(), $depth);

    foreach($this->children($node) as $child)
      $this->rebuildBounds($child, $depth + 1);

    $node->setAttribute($node->getRightColumnName(), $this->getNextBound($k));

    $node->save();
  }

  /**
   * Return all children for the specified node.
   *
   * @param   Baum\Node $node
   * @return  Illuminate\Database\Eloquent\Collection
   */
  public function children($node) {
    $query = $this->node->newQuery();

    $query->where($this->node->getQualifiedParentColumnName(), '=', $node->getKey());

    // We must also add the scoped column values to the query to compute valid
    // left and right indexes.
    foreach($this->scopedAttributes($node) as $fld => $value)
      $query->where($this->qualify($fld), '=', $value);

    $query->orderBy($this->node->getQualifiedLeftColumnName());
    $query->orderBy($this->node->getQualifiedRightColumnName());
    $query->orderBy($this->node->getQualifiedKeyName());

    return $query->get();
  }

  /**
   * Return an array of the scoped attributes of the supplied node.
   *
   * @param   Baum\Node $node
   * @return  array
   */
  protected function scopedAttributes($node) {
    $keys = $this->node->getScopedColumns();

    if ( count($keys) == 0 )
      return array();

    $values = array_map(function($column) use ($node) {
      return $node->getAttribute($column); }, $keys);

    return array_combine($keys, $values);
  }

  /**
   * Return a string-key for the current scoped attributes. Used for index
   * computing when a scope is defined (acsts as an scope identifier).
   *
   * @param   Baum\Node $node
   * @return  string
   */
  protected function scopedKey($node) {
    $attributes = $this->scopedAttributes($node);

    $output = array();

    foreach($attributes as $fld => $value)
      $output[] = $this->qualify($fld).'='.(is_null($value) ? 'NULL' : $value);

    // NOTE: Maybe an md5 or something would be better. Should be unique though.
    return implode(',', $output);
  }

  /**
   * Return next index bound value for the given key (current scope identifier)
   *
   * @param   string  $key
   * @return  integer
   */
  protected function getNextBound($key) {
    if ( false === array_key_exists($key, $this->bounds) )
      $this->bounds[$key] = 0;

    $this->bounds[$key] = $this->bounds[$key] + 1;

    return $this->bounds[$key];
  }

  /**
   * Get the fully qualified value for the specified column.
   *
   * @return string
   */
  protected function qualify($column) {
    return $this->node->getTable() . '.' . $column;
  }

}


<?php
namespace Baum;

use \Illuminate\Events\Dispatcher;

/**
* Move
*/
class Move {

  /**
  * Node on which the move operation will be performed
  *
  * @var \Baum\Node
  */
  protected $node = NULL;

  /**
  * Destination node
  *
  * @var \Baum\Node | int
  */
  protected $target = NULL;

  /**
  * Move target position, one of: child, left, right, root
  *
  * @var string
  */
  protected $position = NULL;

  /**
  * Memoized 1st boundary.
  *
  * @var int
  */
  protected $_bound1 = NULL;

  /**
  * Memoized 2nd boundary.
  *
  * @var int
  */
  protected $_bound2 = NULL;

  /**
  * Memoized boundaries array.
  *
  * @var array
  */
  protected $_boundaries = NULL;

  /**
   * The event dispatcher instance.
   *
   * @var \Illuminate\Events\Dispatcher
   */
  protected static $dispatcher;

  /**
   * Create a new Move class instance.
   *
   * @param   \Baum\Node      $node
   * @param   \Baum\Node|int  $target
   * @param   string          $position
   * @return  void
   */
  public function __construct($node, $target, $position) {
    $this->node     = $node;
    $this->target   = $this->resolveNode($target);
    $this->position = $position;

    $this->setEventDispatcher($node->getEventDispatcher());
  }

  /**
   * Easy static accessor for performing a move operation.
   *
   * @param   \Baum\Node      $node
   * @param   \Baum\Node|int  $target
   * @param   string          $position
   * @return \Baum\Node
   */
  public static function to($node, $target, $position) {
    $instance = new static($node, $target, $position);

    return $instance->perform();
  }

  /**
   * Perform the move operation.
   *
   * @return \Baum\Node
   */
  public function perform() {
    $this->guardAgainstImpossibleMove();

    if ( $this->fireMoveEvent('moving') === false )
      return $this->node;

    if ( $this->hasChange() ) {
      $self = $this;

      $this->node->getConnection()->transaction(function() use ($self) {
        $self->updateStructure();
      });

      $this->target->reload();

      $this->node->setDepthWithSubtree();

      $this->node->reload();
    }

    $this->fireMoveEvent('moved', false);

    return $this->node;
  }

  /**
   * Runs the SQL query associated with the update of the indexes affected
   * by the move operation.
   *
   * @return int
   */
  public function updateStructure() {
    list($a, $b, $c, $d) = $this->boundaries();

    // select the rows between the leftmost & the rightmost boundaries and apply a lock
    $this->applyLockBetween($a, $d);

    $connection = $this->node->getConnection();
    $grammar    = $connection->getQueryGrammar();

    $currentId      = $this->quoteIdentifier($this->node->getKey());
    $parentId       = $this->quoteIdentifier($this->parentId());
    $leftColumn     = $this->node->getLeftColumnName();
    $rightColumn    = $this->node->getRightColumnName();
    $parentColumn   = $this->node->getParentColumnName();
    $wrappedLeft    = $grammar->wrap($leftColumn);
    $wrappedRight   = $grammar->wrap($rightColumn);
    $wrappedParent  = $grammar->wrap($parentColumn);
    $wrappedId      = $grammar->wrap($this->node->getKeyName());

    $lftSql = "CASE
      WHEN $wrappedLeft BETWEEN $a AND $b THEN $wrappedLeft + $d - $b
      WHEN $wrappedLeft BETWEEN $c AND $d THEN $wrappedLeft + $a - $c
      ELSE $wrappedLeft END";

    $rgtSql = "CASE
      WHEN $wrappedRight BETWEEN $a AND $b THEN $wrappedRight + $d - $b
      WHEN $wrappedRight BETWEEN $c AND $d THEN $wrappedRight + $a - $c
      ELSE $wrappedRight END";

    $parentSql = "CASE
      WHEN $wrappedId = $currentId THEN $parentId
      ELSE $wrappedParent END";

    $updateConditions = array(
      $leftColumn   => $connection->raw($lftSql),
      $rightColumn  => $connection->raw($rgtSql),
      $parentColumn => $connection->raw($parentSql)
    );

    if ( $this->node->timestamps )
      $updateConditions[$this->node->getUpdatedAtColumn()] = $this->node->freshTimestamp();

    return $this->node
                ->newNestedSetQuery()
                ->where(function($query) use ($leftColumn, $rightColumn, $a, $d) {
                  $query->whereBetween($leftColumn, array($a, $d))
                        ->orWhereBetween($rightColumn, array($a, $d));
                })
                ->update($updateConditions);
  }

  /**
   * Resolves suplied node. Basically returns the node unchanged if
   * supplied parameter is an instance of \Baum\Node. Otherwise it will try
   * to find the node in the database.
   *
   * @param   \Baum\node|int
   * @return  \Baum\Node
   */
  protected function resolveNode($node) {
    if ( $node instanceof \Baum\Node ) return $node->reload();

    return $this->node->newNestedSetQuery()->find($node);
  }

  /**
   * Check wether the current move is possible and if not, rais an exception.
   *
   * @return void
   */
  protected function guardAgainstImpossibleMove() {
    if ( !$this->node->exists )
      throw new MoveNotPossibleException('A new node cannot be moved.');

    if ( array_search($this->position, array('child', 'left', 'right', 'root')) === FALSE )
      throw new MoveNotPossibleException("Position should be one of ['child', 'left', 'right'] but is {$this->position}.");

    if ( !$this->promotingToRoot() ) {
      if ( is_null($this->target) ) {
        if ( $this->position === 'left' || $this->position === 'right' )
          throw new MoveNotPossibleException("Could not resolve target node. This node cannot move any further to the {$this->position}.");
        else
          throw new MoveNotPossibleException('Could not resolve target node.');
      }

      if ( $this->node->equals($this->target) )
        throw new MoveNotPossibleException('A node cannot be moved to itself.');

      if ( $this->target->insideSubtree($this->node) )
        throw new MoveNotPossibleException('A node cannot be moved to a descendant of itself (inside moved tree).');

      if ( !$this->node->inSameScope($this->target) )
        throw new MoveNotPossibleException('A node cannot be moved to a different scope.');
    }
  }

  /**
   * Computes the boundary.
   *
   * @return int
   */
  protected function bound1() {
    if ( !is_null($this->_bound1) ) return $this->_bound1;

    switch ( $this->position ) {
      case 'child':
        $this->_bound1 = $this->target->getRight();
        break;

      case 'left':
        $this->_bound1 = $this->target->getLeft();
        break;

      case 'right':
        $this->_bound1 = $this->target->getRight() + 1;
        break;

      case 'root':
        $this->_bound1 = $this->node->newNestedSetQuery()->max($this->node->getRightColumnName()) + 1;
        break;
    }

    $this->_bound1 = (($this->_bound1 > $this->node->getRight()) ? $this->_bound1 - 1 : $this->_bound1);
    return $this->_bound1;
  }

  /**
   * Computes the other boundary.
   * TODO: Maybe find a better name for this... ¿?
   *
   * @return int
   */
  protected function bound2() {
    if ( !is_null($this->_bound2) ) return $this->_bound2;

    $this->_bound2 = (($this->bound1() > $this->node->getRight()) ? $this->node->getRight() + 1 : $this->node->getLeft() - 1);
    return $this->_bound2;
  }

  /**
   * Computes the boundaries array.
   *
   * @return array
   */
  protected function boundaries() {
    if ( !is_null($this->_boundaries) ) return $this->_boundaries;

    // we have defined the boundaries of two non-overlapping intervals,
    // so sorting puts both the intervals and their boundaries in order
    $this->_boundaries = array(
      $this->node->getLeft()  ,
      $this->node->getRight() ,
      $this->bound1()         ,
      $this->bound2()
    );
    sort($this->_boundaries);

    return $this->_boundaries;
  }

  /**
   * Computes the new parent id for the node being moved.
   *
   * @return int
   */
  protected function parentId() {
    switch( $this->position ) {
      case 'root':
        return NULL;

      case 'child':
        return $this->target->getKey();

      default:
        return $this->target->getParentId();
    }
  }

  /**
   * Check wether there should be changes in the downward tree structure.
   *
   * @return boolean
   */
  protected function hasChange() {
    return !( $this->bound1() == $this->node->getRight() || $this->bound1() == $this->node->getLeft() );
  }

  /**
   * Check if we are promoting the provided instance to a root node.
   *
   * @return boolean
   */
  protected function promotingToRoot() {
    return ($this->position == 'root');
  }

  /**
   * Get the event dispatcher instance.
   *
   * @return \Illuminate\Events\Dispatcher
   */
  public static function getEventDispatcher() {
    return static::$dispatcher;
  }

  /**
   * Set the event dispatcher instance.
   *
   * @param  \Illuminate\Events\Dispatcher
   * @return void
   */
  public static function setEventDispatcher(Dispatcher $dispatcher) {
    static::$dispatcher = $dispatcher;
  }

  /**
   * Fire the given move event for the model.
   *
   * @param  string $event
   * @param  bool   $halt
   * @return mixed
   */
  protected function fireMoveEvent($event, $halt = true) {
    if ( !isset(static::$dispatcher) ) return true;

    // Basically the same as \Illuminate\Database\Eloquent\Model->fireModelEvent
    // but we relay the event into the node instance.
    $event = "eloquent.{$event}: ".get_class($this->node);

    $method = $halt ? 'until' : 'fire';

    return static::$dispatcher->$method($event, $this->node);
  }

  /**
   * Quotes an identifier for being used in a database query.
   *
   * @param mixed $value
   * @return string
   */
  protected function quoteIdentifier($value) {
    if ( is_null($value) )
      return 'NULL';

    $connection = $this->node->getConnection();

    $pdo = $connection->getPdo();

    return $pdo->quote($value);
  }

  /**
   * Applies a lock to the rows between the supplied index boundaries.
   *
   * @param   int   $lft
   * @param   int   $rgt
   * @return  void
   */
  protected function applyLockBetween($lft, $rgt) {
    $this->node->newQuery()
      ->where($this->node->getLeftColumnName(), '>=', $lft)
      ->where($this->node->getRightColumnName(), '<=', $rgt)
      ->select($this->node->getKeyName())
      ->lockForUpdate()
      ->get();
  }
}


<?php
namespace Baum\Console;

use Baum\Generators\MigrationGenerator;
use Baum\Generators\ModelGenerator;
use Illuminate\Console\Command;
use Symfony\Component\Console\Input\InputArgument;

class InstallCommand extends Command {

  /**
   * The console command name.
   *
   * @var string
   */
  protected $name = 'baum:install';

  /**
   * The console command description.
   *
   * @var string
   */
  protected $description = 'Scaffolds a new migration and model suitable for Baum.';

  /**
   * Migration generator instance
   *
   * @var Baum\Generators\MigrationGenerator
   */
  protected $migrator;

  /**
   * Model generator instance
   *
   * @var Baum\Generators\ModelGenerator
   */
  protected $modeler;

  /**
   * Create a new command instance
   *
   * @return void
   */
  public function __construct(MigrationGenerator $migrator, ModelGenerator $modeler) {
    parent::__construct();

    $this->migrator = $migrator;
    $this->modeler  = $modeler;
  }

  /**
   * Execute the console command.
   *
   * Basically, we'll write the migration and model stubs out to disk inflected
   * with the name provided. Once its done, we'll `dump-autoload` for the entire
   * framework to make sure that the new classes are registered by the class
   * loaders.
   *
   * @return void
   */
  public function fire() {
    $name = $this->input->getArgument('name');

    $this->writeMigration($name);

    $this->writeModel($name);

  }

  /**
   * Get the command arguments
   *
   * @return array
   */
  protected function getArguments() {
    return array(
      array('name', InputArgument::REQUIRED, 'Name to use for the scaffolding of the migration and model.')
    );
  }

  /**
   * Write the migration file to disk.
   *
   * @param  string  $name
   * @return string
   */
  protected function writeMigration($name) {
    $output = pathinfo($this->migrator->create($name, $this->getMigrationsPath()), PATHINFO_FILENAME);

    $this->line("      <fg=green;options=bold>create</fg=green;options=bold>  $output");
  }

  /**
   * Write the model file to disk.
   *
   * @param  string  $name
   * @return string
   */
  protected function writeModel($name) {
    $output = pathinfo($this->modeler->create($name, $this->getModelsPath()), PATHINFO_FILENAME);

    $this->line("      <fg=green;options=bold>create</fg=green;options=bold>  $output");
  }

  /**
   * Get the path to the migrations directory.
   *
   * @return string
   */
  protected function getMigrationsPath() {
    return $this->laravel['path.database'].'/migrations';
  }

  /**
   * Get the path to the models directory.
   *
   * @return string
   */
  protected function getModelsPath() {
    return $this->laravel['path.base'];
  }

}


<?php
namespace Baum;

use Illuminate\Database\Capsule\Manager as Capsule;

class SetValidator {

  /**
  * Node instance for reference
  *
  * @var \Baum\Node
  */
  protected $node = NULL;

  /**
   * Create a new \Baum\SetValidator class instance.
   *
   * @param   \Baum\Node      $node
   * @return  void
   */
  public function __construct($node) {
    $this->node = $node;
  }

  /**
   * Determine if the validation passes.
   *
   * @return boolean
   */
  public function passes() {
    return $this->validateBounds() && $this->validateDuplicates() &&
      $this->validateRoots();
  }

  /**
   * Determine if validation fails.
   *
   * @return boolean
   */
  public function fails() {
    return !$this->passes();
  }

  /**
   * Validates bounds of the nested tree structure. It will perform checks on
   * the `lft`, `rgt` and `parent_id` columns. Mainly that they're not null,
   * rights greater than lefts, and that they're within the bounds of the parent.
   *
   * @return boolean
   */
  protected function validateBounds() {
    $connection = $this->node->getConnection();
    $grammar    = $connection->getQueryGrammar();

    $tableName      = $this->node->getTable();
    $primaryKeyName = $this->node->getKeyName();
    $parentColumn   = $this->node->getQualifiedParentColumnName();

    $lftCol = $grammar->wrap($this->node->getLeftColumnName());
    $rgtCol = $grammar->wrap($this->node->getRightColumnName());

    $qualifiedLftCol    = $grammar->wrap($this->node->getQualifiedLeftColumnName());
    $qualifiedRgtCol    = $grammar->wrap($this->node->getQualifiedRightColumnName());
    $qualifiedParentCol = $grammar->wrap($this->node->getQualifiedParentColumnName());

    $whereStm = "($qualifiedLftCol IS NULL OR
      $qualifiedRgtCol IS NULL OR
      $qualifiedLftCol >= $qualifiedRgtCol OR
      ($qualifiedParentCol IS NOT NULL AND
        ($qualifiedLftCol <= parent.$lftCol OR
          $qualifiedRgtCol >= parent.$rgtCol)))";

    $query = $this->node->newQuery()
      ->join($connection->raw($grammar->wrapTable($tableName).' AS parent'),
             $parentColumn, '=', $connection->raw('parent.'.$grammar->wrap($primaryKeyName)),
             'left outer')
      ->whereRaw($whereStm);

    return ($query->count() == 0);
  }

  /**
   * Checks that there are no duplicates for the `lft` and `rgt` columns.
   *
   * @return boolean
   */
  protected function validateDuplicates() {
    return (
      !$this->duplicatesExistForColumn($this->node->getQualifiedLeftColumnName()) &&
      !$this->duplicatesExistForColumn($this->node->getQualifiedRightColumnName())
    );
  }

  /**
   * For each root of the whole nested set tree structure, checks that their
   * `lft` and `rgt` bounds are properly set.
   *
   * @return boolean
   */
  protected function validateRoots() {
    $roots = forward_static_call(array(get_class($this->node), 'roots'))->get();

    // If a scope is defined in the model we should check that the roots are
    // valid *for each* value in the scope columns.
    if ( $this->node->isScoped() )
      return $this->validateRootsByScope($roots);

    return $this->isEachRootValid($roots);
  }

  /**
   * Checks if duplicate values for the column specified exist. Takes
   * the Nested Set scope columns into account (if appropiate).
   *
   * @param   string  $column
   * @return  boolean
   */
  protected function duplicatesExistForColumn($column) {
    $connection = $this->node->getConnection();
    $grammar    = $connection->getQueryGrammar();

    $columns = array_merge($this->node->getQualifiedScopedColumns(), array($column));

    $columnsForSelect = implode(', ', array_map(function($col) use ($grammar) {
      return $grammar->wrap($col); }, $columns));

    $wrappedColumn = $grammar->wrap($column);

    $query = $this->node->newQuery()
      ->select($connection->raw("$columnsForSelect, COUNT($wrappedColumn)"))
      ->havingRaw("COUNT($wrappedColumn) > 1");

    foreach($columns as $col)
      $query->groupBy($col);

    $result = $query->first();

    return !is_null($result);
  }

  /**
   * Check that each root node in the list supplied satisfies that its bounds
   * values (lft, rgt indexes) are less than the next.
   *
   * @param   mixed   $roots
   * @return  boolean
   */
  protected function isEachRootValid($roots) {
    $left = $right = 0;

    foreach($roots as $root) {
      $rootLeft   = $root->getLeft();
      $rootRight  = $root->getRight();

      if ( !($rootLeft > $left && $rootRight > $right) )
        return false;

      $left   = $rootLeft;
      $right  = $rootRight;
    }

    return true;
  }

  /**
   * Check that each root node in the list supplied satisfies that its bounds
   * values (lft, rgt indexes) are less than the next *within each scope*.
   *
   * @param   mixed   $roots
   * @return  boolean
   */
  protected function validateRootsByScope($roots) {
    foreach($this->groupRootsByScope($roots) as $scope => $groupedRoots) {
      $valid = $this->isEachRootValid($groupedRoots);

      if ( !$valid )
        return false;
    }

    return true;
  }

  /**
   * Given a list of root nodes, it returns an array in which the keys are the
   * array of the actual scope column values and the values are the root nodes
   * inside that scope themselves
   *
   * @param   mixed   $roots
   * @return  array
   */
  protected function groupRootsByScope($roots) {
    $rootsGroupedByScope = array();

    foreach($roots as $root) {
      $key = $this->keyForScope($root);

      if ( !isset($rootsGroupedByScope[$key]) )
        $rootsGroupedByScope[$key] = array();

      $rootsGroupedByScope[$key][] = $root;
    }

    return $rootsGroupedByScope;
  }

  /**
   * Builds a single string for the given scope columns values. Useful for
   * making array keys for grouping.
   *
   * @param Baum\Node   $node
   * @return string
   */
  protected function keyForScope($node) {
    return implode('-', array_map(function($column) use ($node) {
      $value = $node->getAttribute($column);

      if ( is_null($value) )
        return 'NULL';

      return $value;
    }, $node->getScopedColumns()));
  }

}


<?php
namespace Baum;

use \Closure;
use Illuminate\Support\Contracts\ArrayableInterface;
use Baum\Node;

class SetMapper {

  /**
  * Node instance for reference
  *
  * @var \Baum\Node
  */
  protected $node = NULL;

  /**
   * Children key name
   *
   * @var string
   */
  protected $childrenKeyName = 'children';

  /**
   * Create a new \Baum\SetBuilder class instance.
   *
   * @param   \Baum\Node      $node
   * @return  void
   */
  public function __construct($node, $childrenKeyName = 'children') {
    $this->node = $node;

    $this->childrenKeyName = $childrenKeyName;
  }

  /**
   * Maps a tree structure into the database. Unguards & wraps in transaction.
   *
   * @param   array|\Illuminate\Support\Contracts\ArrayableInterface
   * @return  boolean
   */
  public function map($nodeList) {
    $self = $this;

    return $this->wrapInTransaction(function() use ($self, $nodeList) {
      forward_static_call(array(get_class($self->node), 'unguard'));

      $result = $self->mapTree($nodeList);

      forward_static_call(array(get_class($self->node), 'reguard'));

      return $result;
    });
  }

  /**
   * Maps a tree structure into the database without unguarding nor wrapping
   * inside a transaction.
   *
   * @param   array|\Illuminate\Support\Contracts\ArrayableInterface
   * @return  boolean
   */
  public function mapTree($nodeList) {
    $tree = $nodeList instanceof ArrayableInterface ? $nodeList->toArray() : $nodeList;

    $affectedKeys = array();

    $result = $this->mapTreeRecursive($tree, $this->node->getKey(), $affectedKeys);

    if ( $result && count($affectedKeys) > 0 )
      $this->deleteUnaffected($affectedKeys);

    return $result;
  }

  /**
   * Returns the children key name to use on the mapping array
   *
   * @return string
   */
  public function getChildrenKeyName() {
    return $this->childrenKeyName;
  }

  /**
   * Maps a tree structure into the database
   *
   * @param   array   $tree
   * @param   mixed   $parent
   * @return  boolean
   */
  protected function mapTreeRecursive(array $tree, $parentKey = null, &$affectedKeys = array()) {
    // For every attribute entry: We'll need to instantiate a new node either
    // from the database (if the primary key was supplied) or a new instance. Then,
    // append all the remaining data attributes (including the `parent_id` if
    // present) and save it. Finally, tail-recurse performing the same
    // operations for any child node present. Setting the `parent_id` property at
    // each level will take care of the nesting work for us.
    foreach($tree as $attributes) {
      $node = $this->firstOrNew($this->getSearchAttributes($attributes));

      $data = $this->getDataAttributes($attributes);
      if ( !is_null($parentKey) )
        $data[$node->getParentColumnName()] = $parentKey;

      $node->fill($data);

      $result = $node->save();

      if ( ! $result ) return false;

      $affectedKeys[] = $node->getKey();

      if ( array_key_exists($this->getChildrenKeyName(), $attributes) ) {
        $children = $attributes[$this->getChildrenKeyName()];

        if ( count($children) > 0 ) {
          $result = $this->mapTreeRecursive($children, $node->getKey(), $affectedKeys);

          if ( ! $result ) return false;
        }
      }
    }

    return true;
  }

  protected function getSearchAttributes($attributes) {
    $searchable = array($this->node->getKeyName());

    return array_only($attributes, $searchable);
  }

  protected function getDataAttributes($attributes) {
    $exceptions = array($this->node->getKeyName(), $this->getChildrenKeyName());

    return array_except($attributes, $exceptions);
  }

  protected function firstOrNew($attributes) {
    $className = get_class($this->node);

    if ( count($attributes) === 0 )
      return new $className;

    return forward_static_call(array($className, 'firstOrNew'), $attributes);
  }

  protected function pruneScope() {
    if ( $this->node->exists )
      return $this->node->descendants();

    return $this->node->newNestedSetQuery();
  }

  protected function deleteUnaffected($keys = array()) {
    return $this->pruneScope()->whereNotIn($this->node->getKeyName(), $keys)->delete();
  }

  protected function wrapInTransaction(Closure $callback) {
    return $this->node->getConnection()->transaction($callback);
  }

}


<?php
namespace Baum;

use Baum\Extensions\Eloquent\Collection;
use Baum\Extensions\Eloquent\Model;

/**
 * Node
 *
 * This abstract class implements Nested Set functionality. A Nested Set is a
 * smart way to implement an ordered tree with the added benefit that you can
 * select all of their descendants with a single query. Drawbacks are that
 * insertion or move operations need more complex sql queries.
 *
 * Nested sets are appropiate when you want either an ordered tree (menus,
 * commercial categories, etc.) or an efficient way of querying big trees.
 */
abstract class Node extends Model {

  /**
   * Column name to store the reference to parent's node.
   *
   * @var string
   */
  protected $parentColumn = 'parent_id';

  /**
   * Column name for left index.
   *
   * @var string
   */
  protected $leftColumn = 'lft';

  /**
   * Column name for right index.
   *
   * @var string
   */
  protected $rightColumn = 'rgt';

  /**
   * Column name for depth field.
   *
   * @var string
   */
  protected $depthColumn = 'depth';

  /**
   * Column to perform the default sorting
   *
   * @var string
   */
  protected $orderColumn = null;

  /**
  * Guard NestedSet fields from mass-assignment.
  *
  * @var array
  */
  protected $guarded = array('id', 'parent_id', 'lft', 'rgt', 'depth');

  /**
   * Indicates whether we should move to a new parent.
   *
   * @var int
   */
  protected static $moveToNewParentId = NULL;

  /**
   * Columns which restrict what we consider our Nested Set list
   *
   * @var array
   */
  protected $scoped = array();

  /**
   * The "booting" method of the model.
   *
   * We'll use this method to register event listeners on a Node instance as
   * suggested in the beta documentation...
   *
   * TODO:
   *
   *    - Find a way to avoid needing to declare the called methods "public"
   *    as registering the event listeners *inside* this methods does not give
   *    us an object context.
   *
   * Events:
   *
   *    1. "creating": Before creating a new Node we'll assign a default value
   *    for the left and right indexes.
   *
   *    2. "saving": Before saving, we'll perform a check to see if we have to
   *    move to another parent.
   *
   *    3. "saved": Move to the new parent after saving if needed and re-set
   *    depth.
   *
   *    4. "deleting": Before delete we should prune all children and update
   *    the left and right indexes for the remaining nodes.
   *
   *    5. (optional) "restoring": Before a soft-delete node restore operation,
   *    shift its siblings.
   *
   *    6. (optional) "restore": After having restored a soft-deleted node,
   *    restore all of its descendants.
   *
   * @return void
   */
  protected static function boot() {
    parent::boot();

    static::creating(function($node) {
      $node->setDefaultLeftAndRight();
    });

    static::saving(function($node) {
      $node->storeNewParent();
    });

    static::saved(function($node) {
      $node->moveToNewParent();
      $node->setDepth();
    });

    static::deleting(function($node) {
      $node->destroyDescendants();
    });

    if ( static::softDeletesEnabled() ) {
      static::restoring(function($node) {
        $node->shiftSiblingsForRestore();
      });

      static::restored(function($node) {
        $node->restoreDescendants();
      });
    }
  }

  /**
  * Get the parent column name.
  *
  * @return string
  */
  public function getParentColumnName() {
    return $this->parentColumn;
  }

  /**
  * Get the table qualified parent column name.
  *
  * @return string
  */
  public function getQualifiedParentColumnName() {
    return $this->getTable(). '.' .$this->getParentColumnName();
  }

  /**
  * Get the value of the models "parent_id" field.
  *
  * @return int
  */
  public function getParentId() {
    return $this->getAttribute($this->getparentColumnName());
  }

  /**
   * Get the "left" field column name.
   *
   * @return string
   */
  public function getLeftColumnName() {
    return $this->leftColumn;
  }

  /**
   * Get the table qualified "left" field column name.
   *
   * @return string
   */
  public function getQualifiedLeftColumnName() {
    return $this->getTable() . '.' . $this->getLeftColumnName();
  }

  /**
   * Get the value of the model's "left" field.
   *
   * @return int
   */
  public function getLeft() {
    return $this->getAttribute($this->getLeftColumnName());
  }

  /**
   * Get the "right" field column name.
   *
   * @return string
   */
  public function getRightColumnName() {
    return $this->rightColumn;
  }

  /**
   * Get the table qualified "right" field column name.
   *
   * @return string
   */
  public function getQualifiedRightColumnName() {
    return $this->getTable() . '.' . $this->getRightColumnName();
  }

  /**
   * Get the value of the model's "right" field.
   *
   * @return int
   */
  public function getRight() {
    return $this->getAttribute($this->getRightColumnName());
  }

  /**
   * Get the "depth" field column name.
   *
   * @return string
   */
  public function getDepthColumnName() {
    return $this->depthColumn;
  }

  /**
   * Get the table qualified "depth" field column name.
   *
   * @return string
   */
  public function getQualifiedDepthColumnName() {
    return $this->getTable() . '.' . $this->getDepthColumnName();
  }

  /**
   * Get the model's "depth" value.
   *
   * @return int
   */
  public function getDepth() {
    return $this->getAttribute($this->getDepthColumnName());
  }

  /**
   * Get the "order" field column name.
   *
   * @return string
   */
  public function getOrderColumnName() {
    return is_null($this->orderColumn) ? $this->getLeftColumnName() : $this->orderColumn;
  }

  /**
   * Get the table qualified "order" field column name.
   *
   * @return string
   */
  public function getQualifiedOrderColumnName() {
    return $this->getTable() . '.' . $this->getOrderColumnName();
  }

  /**
   * Get the model's "order" value.
   *
   * @return mixed
   */
  public function getOrder() {
    return $this->getAttribute($this->getOrderColumnName());
  }

  /**
   * Get the column names which define our scope
   *
   * @return array
   */
  public function getScopedColumns() {
    return (array) $this->scoped;
  }

  /**
   * Get the qualified column names which define our scope
   *
   * @return array
   */
  public function getQualifiedScopedColumns() {
    if ( !$this->isScoped() )
      return $this->getScopedColumns();

    $prefix = $this->getTable() . '.';

    return array_map(function($c) use ($prefix) {
      return $prefix . $c; }, $this->getScopedColumns());
  }

  /**
   * Returns wether this particular node instance is scoped by certain fields
   * or not.
   *
   * @return boolean
   */
  public function isScoped() {
    return !!(count($this->getScopedColumns()) > 0);
  }

  /**
  * Parent relation (self-referential) 1-1.
  *
  * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
  */
  public function parent() {
    return $this->belongsTo(get_class($this), $this->getParentColumnName());
  }

  /**
  * Children relation (self-referential) 1-N.
  *
  * @return \Illuminate\Database\Eloquent\Relations\HasMany
  */
  public function children() {
    return $this->hasMany(get_class($this), $this->getParentColumnName())
                ->orderBy($this->getOrderColumnName());
  }

  /**
   * Get a new "scoped" query builder for the Node's model.
   *
   * @param  bool  $excludeDeleted
   * @return \Illuminate\Database\Eloquent\Builder|static
   */
  public function newNestedSetQuery($excludeDeleted = true) {
    $builder = $this->newQuery($excludeDeleted)->orderBy($this->getQualifiedOrderColumnName());

    if ( $this->isScoped() ) {
      foreach($this->scoped as $scopeFld)
        $builder->where($scopeFld, '=', $this->$scopeFld);
    }

    return $builder;
  }

  /**
   * Overload new Collection
   *
   * @param array $models
   * @return \Baum\Extensions\Eloquent\Collection
   */
  public function newCollection(array $models = array()) {
    return new Collection($models);
  }

  /**
   * Get all of the nodes from the database.
   *
   * @param  array  $columns
   * @return \Illuminate\Database\Eloquent\Collection|static[]
   */
  public static function all($columns = array('*')) {
    $instance = new static;

    return $instance->newQuery()
                    ->orderBy($instance->getQualifiedOrderColumnName())
                    ->get();
  }

  /**
   * Returns the first root node.
   *
   * @return NestedSet
   */
  public static function root() {
    return static::roots()->first();
  }

  /**
   * Static query scope. Returns a query scope with all root nodes.
   *
   * @return \Illuminate\Database\Query\Builder
   */
  public static function roots() {
    $instance = new static;

    return $instance->newQuery()
                    ->whereNull($instance->getParentColumnName())
                    ->orderBy($instance->getQualifiedOrderColumnName());
  }

  /**
   * Static query scope. Returns a query scope with all nodes which are at
   * the end of a branch.
   *
   * @return \Illuminate\Database\Query\Builder
   */
  public static function allLeaves() {
    $instance = new static;

    $grammar = $instance->getConnection()->getQueryGrammar();

    $rgtCol = $grammar->wrap($instance->getQualifiedRightColumnName());
    $lftCol = $grammar->wrap($instance->getQualifiedLeftColumnName());

    return $instance->newQuery()
                    ->whereRaw($rgtCol . ' - ' . $lftCol . ' = 1')
                    ->orderBy($instance->getQualifiedOrderColumnName());
  }

  /**
   * Static query scope. Returns a query scope with all nodes which are at
   * the middle of a branch (not root and not leaves).
   *
   * @return \Illuminate\Database\Query\Builder
   */
  public static function allTrunks() {
    $instance = new static;

    $grammar = $instance->getConnection()->getQueryGrammar();

    $rgtCol = $grammar->wrap($instance->getQualifiedRightColumnName());
    $lftCol = $grammar->wrap($instance->getQualifiedLeftColumnName());

    return $instance->newQuery()
                    ->whereNotNull($instance->getParentColumnName())
                    ->whereRaw($rgtCol . ' - ' . $lftCol . ' != 1')
                    ->orderBy($instance->getQualifiedOrderColumnName());
  }

  /**
   * Checks wether the underlying Nested Set structure is valid.
   *
   * @return boolean
   */
  public static function isValidNestedSet() {
    $validator = new SetValidator(new static);

    return $validator->passes();
  }

  /**
   * Rebuilds the structure of the current Nested Set.
   *
   * @param  bool $force
   * @return void
   */
  public static function rebuild($force = false) {
    $builder = new SetBuilder(new static);

    $builder->rebuild($force);
  }

  /**
   * Maps the provided tree structure into the database.
   *
   * @param   array|\Illuminate\Support\Contracts\ArrayableInterface
   * @return  boolean
   */
  public static function buildTree($nodeList) {
    return with(new static)->makeTree($nodeList);
  }

  /**
   * Query scope which extracts a certain node object from the current query
   * expression.
   *
   * @return \Illuminate\Database\Query\Builder
   */
  public function scopeWithoutNode($query, $node) {
    return $query->where($node->getKeyName(), '!=', $node->getKey());
  }

  /**
   * Extracts current node (self) from current query expression.
   *
   * @return \Illuminate\Database\Query\Builder
   */
  public function scopeWithoutSelf($query) {
    return $this->scopeWithoutNode($query, $this);
  }

  /**
   * Extracts first root (from the current node p-o-v) from current query
   * expression.
   *
   * @return \Illuminate\Database\Query\Builder
   */
  public function scopeWithoutRoot($query) {
    return $this->scopeWithoutNode($query, $this->getRoot());
  }

  /**
   * Provides a depth level limit for the query.
   *
   * @param   query   \Illuminate\Database\Query\Builder
   * @param   limit   integer
   * @return  \Illuminate\Database\Query\Builder
   */
  public function scopeLimitDepth($query, $limit) {
    $depth  = $this->exists ? $this->getDepth() : $this->getLevel();
    $max    = $depth + $limit;
    $scopes = array($depth, $max);

    return $query->whereBetween($this->getDepthColumnName(), array(min($scopes), max($scopes)));
  }

  /**
   * Returns true if this is a root node.
   *
   * @return boolean
   */
  public function isRoot() {
    return is_null($this->getParentId());
  }

  /**
   * Returns true if this is a leaf node (end of a branch).
   *
   * @return boolean
   */
  public function isLeaf() {
    return $this->exists && ($this->getRight() - $this->getLeft() == 1);
  }

  /**
   * Returns true if this is a trunk node (not root or leaf).
   *
   * @return boolean
   */
  public function isTrunk() {
    return !$this->isRoot() && !$this->isLeaf();
  }

  /**
   * Returns true if this is a child node.
   *
   * @return boolean
   */
  public function isChild() {
    return !$this->isRoot();
  }

  /**
   * Returns the root node starting at the current node.
   *
   * @return NestedSet
   */
  public function getRoot() {
    if ( $this->exists ) {
      return $this->ancestorsAndSelf()->whereNull($this->getParentColumnName())->first();
    } else {
      $parentId = $this->getParentId();

      if ( !is_null($parentId) && $currentParent = static::find($parentId) ) {
        return $currentParent->getRoot();
      } else {
        return $this;
      }
    }
  }

  /**
   * Instance scope which targes all the ancestor chain nodes including
   * the current one.
   *
   * @return \Illuminate\Database\Eloquent\Builder
   */
  public function ancestorsAndSelf() {
    return $this->newNestedSetQuery()
                ->where($this->getLeftColumnName(), '<=', $this->getLeft())
                ->where($this->getRightColumnName(), '>=', $this->getRight());
  }

  /**
   * Get all the ancestor chain from the database including the current node.
   *
   * @param  array  $columns
   * @return \Illuminate\Database\Eloquent\Collection
   */
  public function getAncestorsAndSelf($columns = array('*')) {
    return $this->ancestorsAndSelf()->get($columns);
  }

  /**
   * Get all the ancestor chain from the database including the current node
   * but without the root node.
   *
   * @param  array  $columns
   * @return \Illuminate\Database\Eloquent\Collection
   */
  public function getAncestorsAndSelfWithoutRoot($columns = array('*')) {
    return $this->ancestorsAndSelf()->withoutRoot()->get($columns);
  }

  /**
   * Instance scope which targets all the ancestor chain nodes excluding
   * the current one.
   *
   * @return \Illuminate\Database\Eloquent\Builder
   */
  public function ancestors() {
    return $this->ancestorsAndSelf()->withoutSelf();
  }

  /**
   * Get all the ancestor chain from the database excluding the current node.
   *
   * @param  array  $columns
   * @return \Illuminate\Database\Eloquent\Collection
   */
  public function getAncestors($columns = array('*')) {
    return $this->ancestors()->get($columns);
  }

  /**
   * Get all the ancestor chain from the database excluding the current node
   * and the root node (from the current node's perspective).
   *
   * @param  array  $columns
   * @return \Illuminate\Database\Eloquent\Collection
   */
  public function getAncestorsWithoutRoot($columns = array('*')) {
    return $this->ancestors()->withoutRoot()->get($columns);
  }

  /**
   * Instance scope which targets all children of the parent, including self.
   *
   * @return \Illuminate\Database\Eloquent\Builder
   */
  public function siblingsAndSelf() {
    return $this->newNestedSetQuery()
                ->where($this->getParentColumnName(), $this->getParentId());
  }

  /**
   * Get all children of the parent, including self.
   *
   * @param  array  $columns
   * @return \Illuminate\Database\Eloquent\Collection
   */
  public function getSiblingsAndSelf($columns = array('*')) {
    return $this->siblingsAndSelf()->get($columns);
  }

  /**
   * Instance scope targeting all children of the parent, except self.
   *
   * @return \Illuminate\Database\Eloquent\Builder
   */
  public function siblings() {
    return $this->siblingsAndSelf()->withoutSelf();
  }

  /**
   * Return all children of the parent, except self.
   *
   * @param  array  $columns
   * @return \Illuminate\Database\Eloquent\Collection
   */
  public function getSiblings($columns = array('*')) {
    return $this->siblings()->get($columns);
  }

  /**
   * Instance scope targeting all of its nested children which do not have
   * children.
   *
   * @return \Illuminate\Database\Query\Builder
   */
  public function leaves() {
    $grammar = $this->getConnection()->getQueryGrammar();

    $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName());
    $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName());

    return $this->descendants()
                ->whereRaw($rgtCol . ' - ' . $lftCol . ' = 1');
  }

  /**
   * Return all of its nested children which do not have children.
   *
   * @param  array  $columns
   * @return \Illuminate\Database\Eloquent\Collection
   */
  public function getLeaves($columns = array('*')) {
    return $this->leaves()->get($columns);
  }

  /**
   * Instance scope targeting all of its nested children which are between the
   * root and the leaf nodes (middle branch).
   *
   * @return \Illuminate\Database\Query\Builder
   */
  public function trunks() {
    $grammar = $this->getConnection()->getQueryGrammar();

    $rgtCol = $grammar->wrap($this->getQualifiedRightColumnName());
    $lftCol = $grammar->wrap($this->getQualifiedLeftColumnName());

    return $this->descendants()
                ->whereNotNull($this->getQualifiedParentColumnName())
                ->whereRaw($rgtCol . ' - ' . $lftCol . ' != 1');
  }

  /**
   * Return all of its nested children which are trunks.
   *
   * @param  array  $columns
   * @return \Illuminate\Database\Eloquent\Collection
   */
  public function getTrunks($columns = array('*')) {
    return $this->trunks()->get($columns);
  }

  /**
   * Scope targeting itself and all of its nested children.
   *
   * @return \Illuminate\Database\Query\Builder
   */
  public function descendantsAndSelf() {
    return $this->newNestedSetQuery()
                ->where($this->getLeftColumnName(), '>=', $this->getLeft())
                ->where($this->getLeftColumnName(), '<', $this->getRight());
  }

  /**
   * Retrieve all nested children an self.
   *
   * @param  array  $columns
   * @return \Illuminate\Database\Eloquent\Collection
   */
  public function getDescendantsAndSelf($columns = array('*')) {
    if ( is_array($columns) )
      return $this->descendantsAndSelf()->get($columns);

    $arguments = func_get_args();

    $limit    = intval(array_shift($arguments));
    $columns  = array_shift($arguments) ?: array('*');

    return $this->descendantsAndSelf()->limitDepth($limit)->get($columns);
  }

  /**
   * Set of all children & nested children.
   *
   * @return \Illuminate\Database\Query\Builder
   */
  public function descendants() {
    return $this->descendantsAndSelf()->withoutSelf();
  }

  /**
   * Retrieve all of its children & nested children.
   *
   * @param  array  $columns
   * @return \Illuminate\Database\Eloquent\Collection
   */
  public function getDescendants($columns = array('*')) {
    if ( is_array($columns) )
      return $this->descendants()->get($columns);

    $arguments = func_get_args();

    $limit    = intval(array_shift($arguments));
    $columns  = array_shift($arguments) ?: array('*');

    return $this->descendants()->limitDepth($limit)->get($columns);
  }

  /**
   * Set of "immediate" descendants (aka children), alias for the children relation.
   *
   * @return \Illuminate\Database\Query\Builder
   */
  public function immediateDescendants() {
    return $this->children();
  }

  /**
   * Retrive all of its "immediate" descendants.
   *
   * @param array   $columns
   * @return \Illuminate\Database\Eloquent\Collection
   */
  public function getImmediateDescendants($columns = array('*')) {
    return $this->children()->get($columns);
  }

  /**
  * Returns the level of this node in the tree.
  * Root level is 0.
  *
  * @return int
  */
  public function getLevel() {
    if ( is_null($this->getParentId()) )
      return 0;

    return $this->computeLevel();
  }

  /**
   * Returns true if node is a descendant.
   *
   * @param NestedSet
   * @return boolean
   */
  public function isDescendantOf($other) {
    return (
      $this->getLeft() > $other->getLeft()  &&
      $this->getLeft() < $other->getRight() &&
      $this->inSameScope($other)
    );
  }

  /**
   * Returns true if node is self or a descendant.
   *
   * @param NestedSet
   * @return boolean
   */
  public function isSelfOrDescendantOf($other) {
   return (
      $this->getLeft() >= $other->getLeft() &&
      $this->getLeft() < $other->getRight() &&
      $this->inSameScope($other)
    );
  }

  /**
   * Returns true if node is an ancestor.
   *
   * @param NestedSet
   * @return boolean
   */
  public function isAncestorOf($other) {
    return (
      $this->getLeft() < $other->getLeft()  &&
      $this->getRight() > $other->getLeft() &&
      $this->inSameScope($other)
    );
  }

  /**
   * Returns true if node is self or an ancestor.
   *
   * @param NestedSet
   * @return boolean
   */
  public function isSelfOrAncestorOf($other) {
   return (
      $this->getLeft() <= $other->getLeft() &&
      $this->getRight() > $other->getLeft() &&
      $this->inSameScope($other)
    );
  }

  /**
   * Returns the first sibling to the left.
   *
   * @return NestedSet
   */
  public function getLeftSibling() {
    return $this->siblings()
                ->where($this->getLeftColumnName(), '<', $this->getLeft())
                ->orderBy($this->getOrderColumnName(), 'desc')
                ->get()
                ->last();
  }

  /**
   * Returns the first sibling to the right.
   *
   * @return NestedSet
   */
  public function getRightSibling() {
    return $this->siblings()
                ->where($this->getLeftColumnName(), '>', $this->getLeft())
                ->first();
  }

  /**
   * Find the left sibling and move to left of it.
   *
   * @return \Baum\Node
   */
  public function moveLeft() {
    return $this->moveToLeftOf($this->getLeftSibling());
  }

  /**
   * Find the right sibling and move to the right of it.
   *
   * @return \Baum\Node
   */
  public function moveRight() {
    return $this->moveToRightOf($this->getRightSibling());
  }

  /**
   * Move to the node to the left of ...
   *
   * @return \Baum\Node
   */
  public function moveToLeftOf($node) {
    return $this->moveTo($node, 'left');
  }

  /**
   * Move to the node to the right of ...
   *
   * @return \Baum\Node
   */
  public function moveToRightOf($node) {
    return $this->moveTo($node, 'right');
  }

  /**
   * Alias for moveToRightOf
   *
   * @return \Baum\Node
   */
  public function makeNextSiblingOf($node) {
    return $this->moveToRightOf($node);
  }

  /**
   * Alias for moveToRightOf
   *
   * @return \Baum\Node
   */
  public function makeSiblingOf($node) {
    return $this->moveToRightOf($node);
  }

  /**
   * Alias for moveToLeftOf
   *
   * @return \Baum\Node
   */
  public function makePreviousSiblingOf($node) {
    return $this->moveToLeftOf($node);
  }

  /**
   * Make the node a child of ...
   *
   * @return \Baum\Node
   */
  public function makeChildOf($node) {
    return $this->moveTo($node, 'child');
  }

  /**
   * Make the node the first child of ...
   *
   * @return \Baum\Node
   */
  public function makeFirstChildOf($node) {
    if ( $node->children()->count() == 0 )
      return $this->makeChildOf($node);

    return $this->moveToLeftOf($node->children()->first());
  }

  /**
   * Make the node the last child of ...
   *
   * @return \Baum\Node
   */
  public function makeLastChildOf($node) {
    return $this->makeChildOf($node);
  }

  /**
   * Make current node a root node.
   *
   * @return \Baum\Node
   */
  public function makeRoot() {
    return $this->moveTo($this, 'root');
  }

  /**
   * Equals?
   *
   * @param \Baum\Node
   * @return boolean
   */
  public function equals($node) {
    return ($this == $node);
  }

  /**
   * Checkes if the given node is in the same scope as the current one.
   *
   * @param \Baum\Node
   * @return boolean
   */
  public function inSameScope($other) {
    foreach($this->getScopedColumns() as $fld) {
      if ( $this->$fld != $other->$fld ) return false;
    }

    return true;
  }

  /**
   * Checks wether the given node is a descendant of itself. Basically, whether
   * its in the subtree defined by the left and right indices.
   *
   * @param \Baum\Node
   * @return boolean
   */
  public function insideSubtree($node) {
    return (
      $this->getLeft()  >= $node->getLeft()   &&
      $this->getLeft()  <= $node->getRight()  &&
      $this->getRight() >= $node->getLeft()   &&
      $this->getRight() <= $node->getRight()
    );
  }

  /**
   * Sets default values for left and right fields.
   *
   * @return void
   */
  public function setDefaultLeftAndRight() {
    $withHighestRight = $this->newNestedSetQuery()->reOrderBy($this->getRightColumnName(), 'desc')->take(1)->sharedLock()->first();

    $maxRgt = 0;
    if ( !is_null($withHighestRight) ) $maxRgt = $withHighestRight->getRight();

    $this->setAttribute($this->getLeftColumnName()  , $maxRgt + 1);
    $this->setAttribute($this->getRightColumnName() , $maxRgt + 2);
  }

  /**
   * Store the parent_id if the attribute is modified so as we are able to move
   * the node to this new parent after saving.
   *
   * @return void
   */
  public function storeNewParent() {
    if ( $this->isDirty($this->getParentColumnName()) && ($this->exists || !$this->isRoot()) )
      static::$moveToNewParentId = $this->getParentId();
    else
      static::$moveToNewParentId = FALSE;
  }

  /**
   * Move to the new parent if appropiate.
   *
   * @return void
   */
  public function moveToNewParent() {
    $pid = static::$moveToNewParentId;

    if ( is_null($pid) )
      $this->makeRoot();
    else if ( $pid !== FALSE )
      $this->makeChildOf($pid);
  }

  /**
   * Sets the depth attribute
   *
   * @return \Baum\Node
   */
  public function setDepth() {
    $self = $this;

    $this->getConnection()->transaction(function() use ($self) {
      $self->reload();

      $level = $self->getLevel();

      $self->newNestedSetQuery()->where($self->getKeyName(), '=', $self->getKey())->update(array($self->getDepthColumnName() => $level));
      $self->setAttribute($self->getDepthColumnName(), $level);
    });

    return $this;
  }

  /**
   * Sets the depth attribute for the current node and all of its descendants.
   *
   * @return \Baum\Node
   */
  public function setDepthWithSubtree() {
    $self = $this;

    $this->getConnection()->transaction(function() use ($self) {
      $self->reload();

      $self->descendantsAndSelf()->select($self->getKeyName())->lockForUpdate()->get();

      $oldDepth = !is_null($self->getDepth()) ? $self->getDepth() : 0;

      $newDepth = $self->getLevel();

      $self->newNestedSetQuery()->where($self->getKeyName(), '=', $self->getKey())->update(array($self->getDepthColumnName() => $newDepth));
      $self->setAttribute($self->getDepthColumnName(), $newDepth);

      $diff = $newDepth - $oldDepth;
      if ( !$self->isLeaf() && $diff != 0 )
        $self->descendants()->increment($self->getDepthColumnName(), $diff);
    });

    return $this;
  }

  /**
   * Prunes a branch off the tree, shifting all the elements on the right
   * back to the left so the counts work.
   *
   * @return void;
   */
  public function destroyDescendants() {
    if ( is_null($this->getRight()) || is_null($this->getLeft()) ) return;

    $self = $this;

    $this->getConnection()->transaction(function() use ($self) {
      $self->reload();

      $lftCol = $self->getLeftColumnName();
      $rgtCol = $self->getRightColumnName();
      $lft    = $self->getLeft();
      $rgt    = $self->getRight();

      // Apply a lock to the rows which fall past the deletion point
      $self->newNestedSetQuery()->where($lftCol, '>=', $lft)->select($self->getKeyName())->lockForUpdate()->get();

      // Prune children
      $self->newNestedSetQuery()->where($lftCol, '>', $lft)->where($rgtCol, '<', $rgt)->delete();

      // Update left and right indexes for the remaining nodes
      $diff = $rgt - $lft + 1;

      $self->newNestedSetQuery()->where($lftCol, '>', $rgt)->decrement($lftCol, $diff);
      $self->newNestedSetQuery()->where($rgtCol, '>', $rgt)->decrement($rgtCol, $diff);
    });
  }

  /**
   * "Makes room" for the the current node between its siblings.
   *
   * @return void
   */
  public function shiftSiblingsForRestore() {
    if ( is_null($this->getRight()) || is_null($this->getLeft()) ) return;

    $self = $this;

    $this->getConnection()->transaction(function() use ($self) {
      $lftCol = $self->getLeftColumnName();
      $rgtCol = $self->getRightColumnName();
      $lft    = $self->getLeft();
      $rgt    = $self->getRight();

      $diff = $rgt - $lft + 1;

      $self->newNestedSetQuery()->where($lftCol, '>=', $lft)->increment($lftCol, $diff);
      $self->newNestedSetQuery()->where($rgtCol, '>=', $lft)->increment($rgtCol, $diff);
    });
  }

  /**
   * Restores all of the current node's descendants.
   *
   * @return void
   */
  public function restoreDescendants() {
    if ( is_null($this->getRight()) || is_null($this->getLeft()) ) return;

    $self = $this;

    $this->getConnection()->transaction(function() use ($self) {
      $self->newNestedSetQuery()
        ->withTrashed()
        ->where($self->getLeftColumnName(), '>', $self->getLeft())
        ->where($self->getRightColumnName(), '<', $self->getRight())
        ->update(array(
          $self->getDeletedAtColumn() => null,
          $self->getUpdatedAtColumn() => $self->{$self->getUpdatedAtColumn()}
        ));
    });
  }

  /**
   * Return an key-value array indicating the node's depth with $seperator
   *
   * @return Array
   */
  public static function getNestedList($column, $key = null, $seperator = ' ') {
    $instance = new static;

    $key = $key ?: $instance->getKeyName();
    $depthColumn = $instance->getDepthColumnName();

    $nodes = $instance->newNestedSetQuery()->get()->toArray();

    return array_combine(array_map(function($node) use($key) {
      return $node[$key];
    }, $nodes), array_map(function($node) use($seperator, $depthColumn, $column) {
      return str_repeat($seperator, $node[$depthColumn]) . $node[$column];
    }, $nodes));
  }

  /**
   * Maps the provided tree structure into the database using the current node
   * as the parent. The provided tree structure will be inserted/updated as the
   * descendancy subtree of the current node instance.
   *
   * @param   array|\Illuminate\Support\Contracts\ArrayableInterface
   * @return  boolean
   */
  public function makeTree($nodeList) {
    $mapper = new SetMapper($this);

    return $mapper->map($nodeList);
  }

  /**
   * Main move method. Here we handle all node movements with the corresponding
   * lft/rgt index updates.
   *
   * @param Baum\Node|int $target
   * @param string        $position
   * @return \Baum\Node
   */
  protected function moveTo($target, $position) {
    return Move::to($this, $target, $position);
  }

  /**
   * Compute current node level. If could not move past ourseleves return
   * our ancestor count, otherwhise get the first parent level + the computed
   * nesting.
   *
   * @return integer
   */
  protected function computeLevel() {
    list($node, $nesting) = $this->determineDepth($this);

    if ( $node->equals($this) )
      return $this->ancestors()->count();

    return $node->getLevel() + $nesting;
  }

  /**
   * Return an array with the last node we could reach and its nesting level
   *
   * @param   Baum\Node $node
   * @param   integer   $nesting
   * @return  array
   */
  protected function determineDepth($node, $nesting = 0) {
    // Traverse back up the ancestry chain and add to the nesting level count
    while( $parent = $node->parent()->first() ) {
      $nesting = $nesting + 1;

      $node = $parent;
    }

    return array($node, $nesting);
  }

}


<?php

/*
 * This file is part of the Panther project.
 *
 * (c) Kévin Dunglas <dunglas@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace Symfony\Component\Panther\DomCrawler\Field;

use Symfony\Component\DomCrawler\Field\InputFormField as BaseInputFormField;

/**
 * @author Kévin Dunglas <dunglas@gmail.com>
 */
final class InputFormField extends BaseInputFormField
{
    use FormFieldTrait;

    /**
     * Initializes the form field.
     *
     * @throws \LogicException When node type is incorrect
     */
    protected function initialize()
    {
        $tagName = $this->element->getTagName();
        if ('input' !== $tagName && 'button' !== $tagName) {
            throw new \LogicException(\sprintf('An InputFormField can only be created from an input or button tag (%s given).', $tagName));
        }

        $type = \strtolower($this->element->getAttribute('type'));
        if ('checkbox' === $type) {
            throw new \LogicException('Checkboxes should be instances of ChoiceFormField.');
        }

        if ('file' === $type) {
            throw new \LogicException('File inputs should be instances of FileFormField.');
        }
    }
}


<?php

/*
 * This file is part of the Panther project.
 *
 * (c) Kévin Dunglas <dunglas@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace Symfony\Component\Panther\DomCrawler\Field;

use Facebook\WebDriver\WebDriverSelect;
use Facebook\WebDriver\WebDriverSelectInterface;
use Symfony\Component\DomCrawler\Field\ChoiceFormField as BaseChoiceFormField;
use Symfony\Component\Panther\WebDriver\WebDriverCheckbox;

/**
 * @author Kévin Dunglas <dunglas@gmail.com>
 */
final class ChoiceFormField extends BaseChoiceFormField
{
    use FormFieldTrait;

    /**
     * @var string
     */
    private $type;

    /**
     * @var WebDriverSelectInterface
     */
    private $selector;

    public function hasValue()
    {
        return $this->element->isSelected();
    }

    public function select($value)
    {
        foreach ((array) $value as $v) {
            $this->selector->selectByValue($v);
        }
    }

    /**
     * Ticks a checkbox.
     *
     * @throws \LogicException When the type provided is not correct
     */
    public function tick()
    {
        if ('checkbox' !== $type = $this->element->getAttribute('type')) {
            throw new \LogicException(\sprintf('You cannot tick "%s" as it is not a checkbox (%s).', $this->element->getAttribute('name'), $type));
        }

        $this->setValue(true);
    }

    /**
     * Ticks a checkbox.
     *
     * @throws \LogicException When the type provided is not correct
     */
    public function untick()
    {
        if ('checkbox' !== $type = $this->element->getAttribute('type')) {
            throw new \LogicException(\sprintf('You cannot tick "%s" as it is not a checkbox (%s).', $this->element->getAttribute('name'), $type));
        }

        $this->setValue(false);
    }

    /**
     * Sets the value of the field.
     *
     * @param string|array|bool $value The value of the field
     *
     * @throws \InvalidArgumentException When value type provided is not correct
     */
    public function setValue($value)
    {
        if (\is_bool($value)) {
            if ('checkbox' !== $this->type) {
                throw new \InvalidArgumentException(\sprintf('Invalid argument of type "%s"', \gettype($value)));
            }

            if ($value) {
                if (!$this->element->isSelected()) {
                    $this->element->click();
                }
            } elseif ($this->element->isSelected()) {
                $this->element->click();
            }

            return;
        }

        foreach ((array) $value as $v) {
            $this->selector->selectByValue($v);
        }
    }

    public function addChoice(\DOMElement $node)
    {
        $this->throwNotSupported(__METHOD__);
    }

    /**
     * Returns the type of the choice field (radio, select, or checkbox).
     *
     * @return string The type
     */
    public function getType()
    {
        return $this->type;
    }

    /**
     * Returns true if the field accepts multiple values.
     *
     * @return bool true if the field accepts multiple values, false otherwise
     */
    public function isMultiple()
    {
        return $this->selector->isMultiple();
    }

    /**
     * Returns list of available field options.
     *
     * @return array
     */
    public function availableOptionValues()
    {
        $options = [];

        foreach ($this->selector->getOptions() as $option) {
            $options[] = $option->getAttribute('value');
        }

        return $options;
    }

    /**
     * Disables the internal validation of the field.
     *
     * @return self
     */
    public function disableValidation()
    {
        $this->throwNotSupported(__METHOD__);
    }

    /**
     * Initializes the form field.
     *
     * @throws \LogicException When node type is incorrect
     */
    protected function initialize()
    {
        $tagName = $this->element->getTagName();
        if ('input' !== $tagName && 'select' !== $tagName) {
            throw new \LogicException(\sprintf('A ChoiceFormField can only be created from an input or select tag (%s given).', $tagName));
        }

        $type = \strtolower($this->element->getAttribute('type'));
        if ('input' === $tagName && 'checkbox' !== $type && 'radio' !== $type) {
            throw new \LogicException(\sprintf('A ChoiceFormField can only be created from an input tag with a type of checkbox or radio (given type is %s).', $type));
        }

        $this->type = 'select' === $tagName ? 'select' : $type;
        $this->selector = 'select' === $this->type ? new WebDriverSelect($this->element) : new WebDriverCheckbox($this->element);
    }
}


<?php

/*
 * This file is part of the Panther project.
 *
 * (c) Kévin Dunglas <dunglas@gmail.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */

declare(strict_types=1);

namespace Symfony\Component\Panther;

use Facebook\WebDriver\Exception\NoSuchElementException;
use Facebook\WebDriver\Exception\TimeOutException;
use Facebook\WebDriver\JavaScriptExecutor;
use Facebook\WebDriver\WebDriver;
use Facebook\WebDriver\WebDriverBy;
use Facebook\WebDriver\WebDriverCapabilities;
use Facebook\WebDriver\WebDriverExpectedCondition;
use Facebook\WebDriver\WebDriverHasInputDevices;
use Symfony\Component\BrowserKit\Client as BaseClient;
use Symfony\Component\BrowserKit\Request;
use Symfony\Component\BrowserKit\Response;
use Symfony\Component\DomCrawler\Form;
use Symfony\Component\DomCrawler\Link;
use Symfony\Component\Panther\Cookie\CookieJar;
use Symfony\Component\Panther\DomCrawler\Crawler;
use Symfony\Component\Panther\DomCrawler\Form as PantherForm;
use Symfony\Component\Panther\DomCrawler\Link as PantherLink;
use Symfony\Component\Panther\ProcessManager\BrowserManagerInterface;
use Symfony\Component\Panther\ProcessManager\ChromeManager;
use Symfony\Component\Panther\ProcessManager\SeleniumManager;
use Symfony\Component\Panther\WebDriver\WebDriverMouse;

/**
 * @author Kévin Dunglas <dunglas@gmail.com>
 * @author Dany Maillard <danymaillard93b@gmail.com>
 *
 * @method Crawler getCrawler()
 */
final class Client extends BaseClient implements WebDriver, JavaScriptExecutor, WebDriverHasInputDevices
{
    use ExceptionThrower;

    /**
     * @var WebDriver|null
     */
    private $webDriver;
    private $browserManager;
    private $baseUri;

    /**
     * @param string[]|null $arguments
     */
    public static function createChromeClient(?string $chromeDriverBinary = null, ?array $arguments = null, array $options = [], ?string $baseUri = null): self
    {
        return new self(new ChromeManager($chromeDriverBinary, $arguments, $options), $baseUri);
    }

    public static function createSeleniumClient(?string $host = null, ?WebDriverCapabilities $capabilities = null, ?string $baseUri = null): self
    {
        return new self(new SeleniumManager($host, $capabilities), $baseUri);
    }

    public function __construct(BrowserManagerInterface $browserManager, ?string $baseUri = null)
    {
        $this->browserManager = $browserManager;
        $this->baseUri = $baseUri;
    }

    public function __destruct()
    {
        $this->quit();
    }

    public function start()
    {
        if (null === $this->webDriver) {
            $this->webDriver = $this->browserManager->start();
        }
    }

    public function getRequest()
    {
        throw new \LogicException('HttpFoundation Request object is not available when using WebDriver.');
    }

    public function getResponse()
    {
        throw new \LogicException('HttpFoundation Response object is not available when using WebDriver.');
    }

    public function followRedirects($followRedirect = true): void
    {
        if (!$followRedirect) {
            throw new \InvalidArgumentException('Redirects are always followed when using WebDriver.');
        }
    }

    public function isFollowingRedirects(): bool
    {
        return true;
    }

    public function setMaxRedirects($maxRedirects): void
    {
        if (-1 !== $maxRedirects) {
            throw new \InvalidArgumentException('There are no max redirects when using WebDriver.');
        }
    }

    public function getMaxRedirects(): int
    {
        return -1;
    }

    public function insulate($insulated = true): void
    {
        if (!$insulated) {
            throw new \InvalidArgumentException('Requests are always insulated when using WebDriver.');
        }
    }

    public function setServerParameters(array $server): void
    {
        throw new \InvalidArgumentException('Server parameters cannot be set when using WebDriver.');
    }

    public function setServerParameter($key, $value): void
    {
        throw new \InvalidArgumentException('Server parameters cannot be set when using WebDriver.');
    }

    public function getServerParameter($key, $default = '')
    {
        throw new \InvalidArgumentException('Server parameters cannot be retrieved when using WebDriver.');
    }

    public function click(Link $link)
    {
        if ($link instanceof PantherLink) {
            $link->getElement()->click();

            return $this->crawler = $this->createCrawler();
        }

        return parent::click($link);
    }

    public function submit(Form $form, array $values = [], array $serverParameters = [])
    {
        if ($form instanceof PantherForm) {
            foreach ($values as $field => $value) {
                $form->get($field)->setValue($value);
            }

            $button = $form->getButton();
            null === $button ? $form->getElement()->submit() : $button->click();

            return $this->crawler = $this->createCrawler();
        }

        return parent::submit($form, $values, $serverParameters);
    }

    public function refreshCrawler(): Crawler
    {
        return $this->crawler = $this->createCrawler();
    }

    public function request(string $method, string $uri, array $parameters = [], array $files = [], array $server = [], string $content = null, bool $changeHistory = true): Crawler
    {
        if ('GET' !== $method) {
            throw new \InvalidArgumentException('Only the GET method is supported when using WebDriver.');
        }
        if (null !== $content) {
            throw new \InvalidArgumentException('Setting a content is not supported when using WebDriver.');
        }
        if (!$changeHistory) {
            throw new \InvalidArgumentException('The history always change when using WebDriver.');
        }

        foreach (['parameters', 'files', 'server'] as $arg) {
            if ([] !== $$arg) {
                throw new \InvalidArgumentException(\sprintf('The parameter "$%s" is not supported when using WebDriver.', $arg));
            }
        }

        $this->get($uri);

        return $this->crawler;
    }

    protected function createCrawler(): Crawler
    {
        $this->start();
        $elements = $this->webDriver->findElements(WebDriverBy::cssSelector('html'));

        return new Crawler($elements, $this->webDriver, $this->webDriver->getCurrentURL());
    }

    protected function doRequest($request)
    {
        throw new \LogicException('Not useful in WebDriver mode.');
    }

    public function back()
    {
        $this->start();
        $this->webDriver->navigate()->back();

        return $this->crawler = $this->createCrawler();
    }

    public function forward()
    {
        $this->start();
        $this->webDriver->navigate()->forward();

        return $this->crawler = $this->createCrawler();
    }

    public function reload()
    {
        $this->start();
        $this->webDriver->navigate()->refresh();

        return $this->crawler = $this->createCrawler();
    }

    public function followRedirect()
    {
        throw new \LogicException('Redirects are always followed when using WebDriver.');
    }

    public function restart()
    {
        if (null !== $this->webDriver) {
            $this->webDriver->manage()->deleteAllCookies();
        }

        $this->quit();
        $this->start();
    }

    public function getCookieJar()
    {
        $this->start();

        return new CookieJar($this->webDriver);
    }

    /**
     * @throws NoSuchElementException
     * @throws TimeOutException
     *
     * @return Crawler
     */
    public function waitFor(string $cssSelector, int $timeoutInSecond = 30, int $intervalInMillisecond = 250)
    {
        $this->wait($timeoutInSecond, $intervalInMillisecond)->until(
            WebDriverExpectedCondition::visibilityOfElementLocated(WebDriverBy::cssSelector($cssSelector))
        );

        return $this->crawler = $this->createCrawler();
    }

    public function getWebDriver(): WebDriver
    {
        $this->start();

        return $this->webDriver;
    }

    public function get($uri)
    {
        $this->start();

        // Prepend the base URI to URIs without a host
        if (null !== $this->baseUri && (false !== $components = \parse_url($uri)) && !isset($components['host'])) {
            $uri = $this->baseUri.$uri;
        }

        $this->internalRequest = new Request($uri, 'GET');
        $this->webDriver->get($uri);
        $this->internalResponse = new Response($this->webDriver->getPageSource());
        $this->crawler = $this->createCrawler();

        return $this;
    }

    public function close()
    {
        $this->start();

        return $this->webDriver->close();
    }

    public function getCurrentURL()
    {
        $this->start();

        return $this->webDriver->getCurrentURL();
    }

    public function getPageSource()
    {
        $this->start();

        return $this->webDriver->getPageSource();
    }

    public function getTitle()
    {
        $this->start();

        return $this->webDriver->getTitle();
    }

    public function getWindowHandle()
    {
        $this->start();

        return $this->webDriver->getWindowHandle();
    }

    public function getWindowHandles()
    {
        $this->start();

        return $this->webDriver->getWindowHandles();
    }

    public function quit()
    {
        if (null !== $this->webDriver) {
            $this->webDriver->quit();
            $this->webDriver = null;
        }
        $this->browserManager->quit();
    }

    public function takeScreenshot($saveAs = null)
    {
        $this->start();

        return $this->webDriver->takeScreenshot($saveAs);
    }

    public function wait($timeoutInSecond = 30, $intervalInMillisecond = 250)
    {
        $this->start();

        return $this->webDriver->wait($timeoutInSecond, $intervalInMillisecond);
    }

    public function manage()
    {
        $this->start();

        return $this->webDriver->manage();
    }

    public function navigate()
    {
        $this->start();

        return $this->webDriver->navigate();
    }

    public function switchTo()
    {
        $this->start();

        return $this->webDriver->switchTo();
    }

    public function execute($name, $params)
    {
        $this->start();

        return $this->webDriver->execute($name, $params);
    }

    public function findElement(WebDriverBy $locator)
    {
        $this->start();

        return $this->webDriver->findElement($locator);
    }

    public function findElements(WebDriverBy $locator)
    {
        $this->start();

        return $this->webDriver->findElements($locator);
    }

    public function executeScript($script, array $arguments = [])
    {
        if (!$this->webDriver instanceof JavaScriptExecutor) {
            throw new \RuntimeException(sprintf('"%s" does not implement "%s".', \get_class($this->webDriver), JavaScriptExecutor::class));
        }

        return $this->webDriver->executeScript($script, $arguments);
    }

    public function executeAsyncScript($script, array $arguments = [])
    {
        if (!$this->webDriver instanceof JavaScriptExecutor) {
            throw new \RuntimeException(sprintf('"%s" does not implement "%s".', \get_class($this->webDriver), JavaScriptExecutor::class));
        }

        return $this->webDriver->executeAsyncScript($script, $arguments);
    }

    public function getKeyboard()
    {
        if (!$this->webDriver instanceof WebDriverHasInputDevices) {
            throw new \RuntimeException(sprintf('"%s" does not implement "%s".', \get_class($this->webDriver), WebDriverHasInputDevices::class));
        }

        return $this->webDriver->getKeyboard();
    }

    public function getMouse(): WebDriverMouse
    {
        if (!$this->webDriver instanceof WebDriverHasInputDevices) {
            throw new \RuntimeException(sprintf('"%s" does not implement "%s".', \get_class($this->webDriver), WebDriverHasInputDevices::class));
        }

        return new WebDriverMouse($this->webDriver->getMouse(), $this);
    }
}


<?php

namespace Proengsoft\JsValidation;

use Illuminate\Contracts\Http\Kernel;
use Illuminate\Support\ServiceProvider;
use Proengsoft\JsValidation\Javascript\ValidatorHandler;

class JsValidationServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap the application services.
     *
     * @return void
     */
    public function boot()
    {
        $this->bootstrapConfigs();
        $this->bootstrapViews();
        $this->bootstrapValidator();
        $this->publishAssets();

        if ($this->app['config']->get('jsvalidation.disable_remote_validation') === false) {
            $this->app[Kernel::class]->pushMiddleware(RemoteValidationMiddleware::class);
        }
    }

    /**
     * Register the application services.
     *
     * @return void
     */
    public function register()
    {
        $this->app->singleton('jsvalidator', function ($app) {
            $config = $app['config']->get('jsvalidation');

            return new JsValidatorFactory($app, $config);
        });
    }

    /**
     * Configure and publish views.
     *
     * @return void
     */
    protected function bootstrapViews()
    {
        $viewPath = realpath(__DIR__.'/../resources/views');

        $this->loadViewsFrom($viewPath, 'jsvalidation');
        $this->publishes([
            $viewPath => $this->app['path.base'].'/resources/views/vendor/jsvalidation',
        ], 'views');
    }

    /**
     * Configure Laravel Validator.
     *
     * @return void
     */
    protected function bootstrapValidator()
    {
        $callback = function () {
            return true;
        };
        $this->app['validator']->extend(ValidatorHandler::JSVALIDATION_DISABLE, $callback);
    }

    /**
     * Load and publishes configs.
     *
     * @return void
     */
    protected function bootstrapConfigs()
    {
        $configFile = realpath(__DIR__.'/../config/jsvalidation.php');

        $this->mergeConfigFrom($configFile, 'jsvalidation');
        $this->publishes([$configFile => $this->app['path.config'].'/jsvalidation.php'], 'config');
    }

    /**
     * Publish public assets.
     *
     * @return void
     */
    protected function publishAssets()
    {
        $this->publishes([
            realpath(__DIR__.'/../public') => $this->app['path.public'].'/vendor/jsvalidation',
        ], 'public');
    }
}


<?php

namespace Proengsoft\JsValidation\Javascript;

use Proengsoft\JsValidation\Support\RuleListTrait;
use Proengsoft\JsValidation\Support\DelegatedValidator;
use Proengsoft\JsValidation\Support\UseDelegatedValidatorTrait;

class RuleParser
{
    use JavascriptRulesTrait;
    use RuleListTrait;
    use UseDelegatedValidatorTrait;

    /**
     * Rule used to validate remote requests.
     */
    const REMOTE_RULE = 'laravelValidationRemote';

    /**
     * Rule used to validate javascript fields.
     */
    const JAVASCRIPT_RULE = 'laravelValidation';

    /**
     * Token used to secure romte validations.
     *
     * @var null|string $remoteToken
     */
    protected $remoteToken;

    /**
     * Conditional Validations.
     *
     * @var array
     */
    protected $conditional = [];

    /**
     * Create a new JsValidation instance.
     *
     * @param \Proengsoft\JsValidation\Support\DelegatedValidator $validator
     * @param null|string $remoteToken
     */
    public function __construct(DelegatedValidator $validator, $remoteToken = null)
    {
        $this->validator = $validator;
        $this->remoteToken = $remoteToken;
    }

    /**
     * Return parsed Javascript Rule.
     *
     * @param string $attribute
     * @param string $rule
     * @param $parameters
     * @param $rawRule
     * @return array
     */
    public function getRule($attribute, $rule, $parameters, $rawRule)
    {
        $isConditional = $this->isConditionalRule($attribute, $rawRule);
        $isRemote = $this->isRemoteRule($rule);

        if ($isConditional || $isRemote) {
            list($attribute, $parameters) = $this->remoteRule($attribute, $isConditional);
            $jsRule = self::REMOTE_RULE;
        } else {
            list($jsRule, $attribute, $parameters) = $this->clientRule($attribute, $rule, $parameters);
        }

        $attribute = $this->getAttributeName($attribute);

        return [$attribute, $jsRule, $parameters];
    }

    /**
     * Gets rules from Validator instance.
     *
     * @return array
     */
    public function getValidatorRules()
    {
        return $this->validator->getRules();
    }

    /**
     * Add conditional rules.
     *
     * @param mixed $attribute
     * @param array $rules
     * @return void
     */
    public function addConditionalRules($attribute, $rules = [])
    {
        foreach ((array) $attribute as $key) {
            $current = isset($this->conditional[$key]) ? $this->conditional[$key] : [];
            $merge = head($this->validator->explodeRules((array) $rules));
            $this->conditional[$key] = array_merge($current, $merge);
        }
    }

    /**
     * Determine if rule is passed with sometimes.
     *
     * @param mixed $attribute
     * @param string $rule
     * @return bool
     */
    protected function isConditionalRule($attribute, $rule)
    {
        return isset($this->conditional[$attribute]) &&
        in_array($rule, $this->conditional[$attribute]);
    }

    /**
     * Returns Javascript parameters for remote validated rules.
     *
     * @param string $attribute
     * @param string $rule
     * @param $parameters
     * @return array
     */
    protected function clientRule($attribute, $rule, $parameters)
    {
        $jsRule = self::JAVASCRIPT_RULE;
        $method = "rule{$rule}";

        if (method_exists($this, $method)) {
            list($attribute, $parameters) = $this->$method($attribute, $parameters);
        }

        return [$jsRule, $attribute, $parameters];
    }

    /**
     * Returns Javascript parameters for remote validated rules.
     *
     * @param string $attribute
     * @param bool $forceRemote
     * @return array
     */
    protected function remoteRule($attribute, $forceRemote)
    {
        $attrHtmlName = $this->getAttributeName($attribute);
        $params = [
            $attrHtmlName,
            $this->remoteToken,
            $forceRemote,
        ];

        return [$attribute, $params];
    }

    /**
     * Handles multidimensional attribute names.
     *
     * @param mixed $attribute
     * @return string
     */
    protected function getAttributeName($attribute)
    {
        $attributeArray = explode('.', $attribute);
        if (count($attributeArray) > 1) {
            return $attributeArray[0].'['.implode('][', array_slice($attributeArray, 1)).']';
        }

        return $attribute;
    }

    /**
     * Parse named parameters to $key => $value items.
     *
     * @param array $parameters
     * @return array
     */
    public function parseNamedParameters($parameters)
    {
        return array_reduce($parameters, function ($result, $item) {
            list($key, $value) = array_pad(explode('=', $item, 2), 2, null);

            $result[$key] = $value;

            return $result;
        });
    }
}