diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..732745d --- /dev/null +++ b/.php_cs @@ -0,0 +1,156 @@ + + */ +$fixers = [ + 'align_multiline_comment' => ['comment_type'=>'phpdocs_only'], + 'array_indentation' => true, + 'array_syntax' => ['syntax' => 'short'], + 'binary_operator_spaces' => [ + 'align_equals' => false, + 'align_double_arrow' => true, + ], + 'blank_line_after_namespace' => true, + 'blank_line_after_opening_tag' => true, + 'blank_line_before_statement' => true, + 'braces' => true, + 'cast_spaces' => true, + 'class_attributes_separation' => true, + 'class_definition' => true, + 'compact_nullable_typehint' => true, + 'concat_space' => ['spacing' => 'none'], + 'constant_case' => true, + 'elseif' => true, + 'encoding' => true, + 'explicit_indirect_variable' => true, + 'explicit_string_variable' => true, + 'full_opening_tag' => true, + 'fully_qualified_strict_types' => true, + 'function_declaration' => true, + 'function_typehint_space' => true, + 'include' => true, + 'indentation_type' => true, + 'line_ending' => true, + 'list_syntax' => ['syntax' => 'short'], + 'lowercase_cast' => true, + 'lowercase_constants' => true, + 'lowercase_keywords' => true, + 'lowercase_static_reference' => true, + 'magic_constant_casing' => true, + 'magic_method_casing' => true, + 'method_argument_space' => [ + 'on_multiline' => 'ensure_fully_multiline', + ], + 'method_chaining_indentation' => true, + 'multiline_whitespace_before_semicolons' => true, + 'native_function_casing' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_break_comment' => true, + 'no_closing_tag' => true, + 'no_empty_phpdoc' => true, + 'no_empty_statement' => true, + 'no_extra_blank_lines' => true, + 'no_leading_import_slash' => true, + 'no_leading_namespace_whitespace' => true, + 'no_multiline_whitespace_around_double_arrow' => true, + 'no_null_property_initialization' => true, + 'no_short_bool_cast' => true, + 'no_short_echo_tag' => true, + 'no_singleline_whitespace_before_semicolons' => true, + 'no_spaces_after_function_name' => true, + 'no_spaces_inside_parenthesis' => true, + 'no_superfluous_elseif' => true, + 'no_trailing_comma_in_list_call' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_trailing_whitespace' => true, + 'no_trailing_whitespace_in_comment' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, + 'not_operator_with_successor_space' => true, + 'nullable_type_declaration_for_default_null_value' => ['use_nullable_type_declaration' => true], + 'object_operator_without_whitespace' => true, + 'ordered_class_elements' => [ + 'order' => [ + 'use_trait', + 'constant_public', + 'constant_protected', + 'constant_private', + 'property_public', + 'property_protected', + 'property_private', + 'construct', + 'destruct', + 'phpunit', + 'method_public', + 'method_public_static', + 'magic', + 'method_protected', + 'method_protected_static', + 'method_private', + 'method_private_static', + ], + 'sortAlgorithm' => 'alpha', + ], + 'ordered_imports' => ['sortAlgorithm' => 'length'], + 'php_unit_method_casing' => ['case' => 'snake_case'], + 'phpdoc_add_missing_param_annotation' => true, + 'phpdoc_indent' => true, + 'phpdoc_inline_tag' => true, + 'phpdoc_no_access' => true, + 'phpdoc_no_alias_tag' => true, + 'phpdoc_no_package' => true, + 'phpdoc_order' => true, + 'phpdoc_return_self_reference' => true, + 'phpdoc_scalar' => true, + 'phpdoc_summary' => true, + 'phpdoc_to_comment' => true, + 'phpdoc_trim' => true, + 'phpdoc_types' => true, + 'phpdoc_var_annotation_correct_order' => true, + 'phpdoc_var_without_name' => true, + 'return_assignment' => true, + 'return_type_declaration' => ['space_before' => 'none'], + 'short_scalar_cast' => true, + 'simple_to_complex_string_variable' => true, + 'simplified_null_return' => true, + 'single_blank_line_at_eof' => true, + 'single_blank_line_before_namespace' => true, + 'single_class_element_per_statement' => ['elements' => ['property']], + 'single_import_per_statement' => true, + 'single_line_after_imports' => true, + 'single_quote' => true, + 'standardize_not_equals' => true, + 'switch_case_semicolon_to_colon' => true, + 'switch_case_space' => true, + 'ternary_operator_spaces' => true, + 'ternary_to_null_coalescing' => true, + 'trailing_comma_in_multiline_array' => true, + 'trim_array_spaces' => true, + 'unary_operator_spaces' => true, + 'visibility_required' => true, + 'void_return' => true, +]; + +$finder = PhpCsFixer\Finder::create() + ->in(__DIR__) + ->exclude(['bootstrap', 'storage', 'vendor', 'tests']) + ->notPath('server.php') + ->notPath('public/index.php'); + +return PhpCsFixer\Config::create() + ->setUsingCache(false) + ->setRules($fixers) + ->setRiskyAllowed(true) + ->setFinder($finder); diff --git a/composer.json b/composer.json index a7db3b0..474520b 100755 --- a/composer.json +++ b/composer.json @@ -10,10 +10,10 @@ }], "require": { "php": ">=7.2", - "illuminate/database": "~5.5|~6.0", - "illuminate/support": "~5.5|~6.0", - "illuminate/console": "~5.5|~6.0", - "illuminate/events": "~5.5|~6.0" + "illuminate/database": "~5.5|~6.0|~7.0", + "illuminate/support": "~5.5|~6.0|~7.0", + "illuminate/console": "~5.5|~6.0|~7.0", + "illuminate/events": "~5.5|~6.0|~7.0" }, "require-dev": { "phpunit/phpunit": "~8.0", @@ -38,4 +38,4 @@ ] } } -} \ No newline at end of file +} diff --git a/resources/views/migration.php b/resources/views/migration.php deleted file mode 100755 index 01fc91b..0000000 --- a/resources/views/migration.php +++ /dev/null @@ -1,31 +0,0 @@ - - -use Illuminate\Database\Migrations\Migration; -use Illuminate\Database\Schema\Blueprint; - -class extends Migration -{ - /** - * Make changes to the table. - * - * @return void - */ - public function up() - { - Schema::table('', function (Blueprint $table) { - $table->integer('')->nullable(); - }); - } - - /** - * Revert the changes to the table. - * - * @return void - */ - public function down() - { - Schema::table('', function (Blueprint $table) { - $table->dropColumn(''); - }); - } -} diff --git a/src/Config.php b/src/Config.php index 802a372..8cc041e 100644 --- a/src/Config.php +++ b/src/Config.php @@ -6,43 +6,51 @@ class Config { - const POSITION_TOP = 'top'; + const ADD_NEW_ITEM_TO_KEY = 'addNewItemTo'; + const POSITION_BOTTOM = 'bottom'; - const TOP_POSITION_IN_LIST_KEY = 'topPositionInList'; const POSITION_COLUMN_NAME_KEY = 'positionColumnName'; + + const POSITION_TOP = 'top'; + const SCOPE_KEY = 'scope'; - const ADD_NEW_ITEM_TO_KEY = 'addNewItemTo'; - protected $defaultTopPositionInList = 1; + const TOP_POSITION_IN_LIST_KEY = 'topPositionInList'; + + protected $config = []; + + protected $defaultAddNewItemTo = self::POSITION_BOTTOM; + protected $defaultPositionColumnName = 'position'; + protected $defaultScope = '1 = 1'; - protected $defaultAddNewItemTo = self::POSITION_BOTTOM; - protected $config = []; + protected $defaultTopPositionInList = 1; public function __construct() { $this->config = $this->buildDefaultConfig(); } - public function setTopPositionInList($position) + public function getAddNewItemTo() { - if (! is_int($position)) { - throw new InvalidArgumentException('Only integers are allowed.'); - } + return $this->get(self::ADD_NEW_ITEM_TO_KEY); + } - return $this->set(self::TOP_POSITION_IN_LIST_KEY, $position); + public function getPositionColumnName() + { + return $this->get(self::POSITION_COLUMN_NAME_KEY); } - public function setPositionColumnName($name) + public function getScope() { - return $this->set(self::POSITION_COLUMN_NAME_KEY, $name); + return $this->get(self::SCOPE_KEY); } - public function setScope($scope) + public function getTopPositionInList() { - return $this->set(self::SCOPE_KEY, $scope); + return $this->get(self::TOP_POSITION_IN_LIST_KEY); } public function setAddNewItemTo($listPosition) @@ -54,24 +62,30 @@ public function setAddNewItemTo($listPosition) return $this->set(self::ADD_NEW_ITEM_TO_KEY, $listPosition); } - public function getTopPositionInList() + public function setPositionColumnName($name) { - return $this->get(self::TOP_POSITION_IN_LIST_KEY); + return $this->set(self::POSITION_COLUMN_NAME_KEY, $name); } - public function getPositionColumnName() + public function setScope($scope) { - return $this->get(self::POSITION_COLUMN_NAME_KEY); + return $this->set(self::SCOPE_KEY, $scope); } - public function getScope() + public function setTopPositionInList($position) { - return $this->get(self::SCOPE_KEY); + if (! is_int($position)) { + throw new InvalidArgumentException('Only integers are allowed.'); + } + + return $this->set(self::TOP_POSITION_IN_LIST_KEY, $position); } - public function getAddNewItemTo() + protected function assertKeyIsValid($key): void { - return $this->get(self::ADD_NEW_ITEM_TO_KEY); + if (! in_array($key, $this->validKeys())) { + throw new InvalidArgumentException("Key '{$key}' is invalid."); + } } protected function buildDefaultConfig() @@ -79,8 +93,8 @@ protected function buildDefaultConfig() return [ self::TOP_POSITION_IN_LIST_KEY => $this->defaultTopPositionInList, self::POSITION_COLUMN_NAME_KEY => $this->defaultPositionColumnName, - self::SCOPE_KEY => $this->defaultScope, - self::ADD_NEW_ITEM_TO_KEY => $this->defaultAddNewItemTo, + self::SCOPE_KEY => $this->defaultScope, + self::ADD_NEW_ITEM_TO_KEY => $this->defaultAddNewItemTo, ]; } @@ -99,13 +113,6 @@ protected function set($key, $value) return $this; } - protected function assertKeyIsValid($key) - { - if (! in_array($key, $this->validKeys())) { - throw new InvalidArgumentException("Key '$key' is invalid."); - } - } - protected function validKeys() { return [self::TOP_POSITION_IN_LIST_KEY, self::POSITION_COLUMN_NAME_KEY, self::SCOPE_KEY, self::ADD_NEW_ITEM_TO_KEY]; diff --git a/src/Console/Commands/AttachCommand.php b/src/Console/Commands/AttachCommand.php index 129bdb3..834d502 100644 --- a/src/Console/Commands/AttachCommand.php +++ b/src/Console/Commands/AttachCommand.php @@ -1,38 +1,48 @@ files = $files; } /** @@ -40,32 +50,27 @@ public function __construct() * * @return void */ - public function fire() + public function handle() { try { - DB::table($this->argument('table'))->first(); - - if (! Schema::hasColumn($this->argument('table'), $this->argument('column'))) { - $this->createMigration(); - } else { - $this->error('Table already contains a column called '.$this->argument('column')); + if ($this->shouldCreateMigration()) { + return $this->createMigration(); } + + $this->error('Table already contains a column called '.$this->argument('column')); } catch (Exception $e) { $this->error('No such table found in database: '.$this->argument('table')); } } /** - * Get the console command arguments. + * Get the path to the stubs. * - * @return array + * @return string */ - protected function getArguments() + public function stubPath() { - return [ - ['table', InputArgument::REQUIRED, 'The name of the database table the Listify field will be added to.'], - ['column', InputArgument::OPTIONAL, 'The name of the column to be used by Listify.', 'position'], - ]; + return __DIR__.'/stubs'; } /** @@ -73,32 +78,88 @@ protected function getArguments() * * @return void */ - public function createMigration() + protected function createMigration(): void { - $targetTableClassName = str_replace(' ', '', ucwords(str_replace('_', ' ', $this->argument('table')))); - $targetColumnClassName = str_replace(' ', '', ucwords(str_replace('_', ' ', $this->argument('column')))); - $data = [ - 'targetTableClassName' => $targetTableClassName, - 'targetColumnClassName' => $targetColumnClassName, - 'tableName' => strtolower($this->argument('table')), - 'columnName' => strtolower($this->argument('column')), - ]; - - $prefix = date('Y_m_d_His'); - $path = base_path().'/database/migrations'; - - if (! is_dir($path)) { - mkdir($path); - } + $fileName = $this->getMigrationFileName( + $className = $this->getClassName() + ); - $fileName = $path.'/'.$prefix.'_add_'.$data['columnName'].'_to_'.$data['tableName'].'_table.php'; - $data['className'] = 'Add'.$data['targetColumnClassName'].'To'.$data['targetTableClassName'].'Table'; + $file = str_replace('DummyClass', $className, $this->getStub()); + $file = str_replace('DummyTable', $this->getTableName(), $file); + $file = str_replace('DummyColumn', $this->getColumnName(), $file); - // Save the new migration to disk using the stapler migration view. - $migration = View::make('listify::migration', $data)->render(); - File::put($fileName, $migration); + $this->files->put($fileName, $file); - // Dump the autoloader and print a created migration message to the console. - $this->info("Created migration: $fileName"); + $this->info("Migration created: {$fileName}"); + } + + /** + * Get the migration class name to generate. + * + * @return string + */ + protected function getClassName() + { + $table = $this->getTableName(); + $column = $this->getColumnName(); + + return Str::studly("add_{$column}_to_{$table}"); + } + + /** + * Get the database table column's name. + * + * @return void + */ + protected function getColumnName() + { + return Str::snake($this->argument('column')); + } + + /** + * Get the migration file name. + * + * @param string $className + * @return string + */ + protected function getMigrationFileName($className) + { + $now = date('Y_m_d_His'); + + $file = $now.'_'.Str::snake($className); + + $path = $this->laravel->databasePath('migrations'); + + return "{$path}/{$file}.php"; + } + + /** + * Get the migration file stub. + * + * @return string + */ + protected function getStub() + { + return $this->files->get($this->stubPath().'/migration.stub'); + } + + /** + * Get the database table name. + * + * @return string + */ + protected function getTableName() + { + return Str::snake($this->argument('table')); + } + + /** + * Determine if a new migration should be generated. + * + * @return bool + */ + protected function shouldCreateMigration() + { + return ! Schema::hasColumn($this->getTableName(), $this->getColumnName()); } } diff --git a/src/Console/Commands/stubs/migration.stub b/src/Console/Commands/stubs/migration.stub new file mode 100644 index 0000000..d2863ad --- /dev/null +++ b/src/Console/Commands/stubs/migration.stub @@ -0,0 +1,32 @@ +integer('DummyColumn')->nullable(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::table('DummyTable', function (Blueprint $table) { + $table->removeColumn('DummyColumn'); + }); + } +} diff --git a/src/Listify.php b/src/Listify.php index dfeaac7..ba08cd5 100644 --- a/src/Listify.php +++ b/src/Listify.php @@ -9,15 +9,13 @@ /** * Gives some nice sorting features to a model. - * http://lookitsatravis.github.io/listify. * * Ported from https://github.com/swanandp/acts_as_list * * @version 2.0.0 * * @author Travis Vignon - * - * @link + * @see http://lookitsatravis.github.io/listify */ trait Listify { @@ -35,6 +33,13 @@ trait Listify */ protected $originalAttributesLoaded = false; + /** + * Contains the current raw scope string. Used to check for changes. + * + * @var string + */ + protected $stringScopeValue; + /** * Container for the changed attributes of the model. * @@ -43,47 +48,31 @@ trait Listify protected $swappedAttributes = []; /** - * Contains the current raw scope string. Used to check for changes. + * Returns the value of the 'add_new_at' option. * - * @var string + * @return string */ - protected $stringScopeValue = null; + public function addNewAt() + { + return $this->getListifyConfig()->getAddNewItemTo(); + } /** - * Returns whether the scope has changed during the course of interaction with the model. + * Decrease the position of this item without adjusting the rest of the list. * - * @return bool + * @param int $count default 1 + * + * @return $this */ - public static function bootListify() + public function decrementPosition($count = 1) { - //Bind to model events - static::deleting(function ($model) { - /* @var Listify $model */ - $model->reloadPosition(); - }); - - static::deleted(function ($model) { - /* @var Listify $model */ - $model->decrementPositionsOnLowerItems(); - }); - - static::updating(function ($model) { - /* @var Listify $model */ - $model->checkScope(); - }); + if ($this->isNotInList()) { + return $this; + } - static::updated(function ($model) { - /* @var Listify $model */ - $model->updatePositions(); - }); + $this->setListifyPosition($this->getListifyPosition() - $count); - static::creating(function ($model) { - /* @var Listify $model */ - if ($model->addNewAt()) { - $methodName = 'addToList'.$model->addNewAt(); - $model->$methodName(); - } - }); + return $this; } /** @@ -101,142 +90,229 @@ public function getListifyConfig() } /** - * An Eloquent scope based on the processed scope option. - * - * @param $query An Eloquent Query Builder instance + * Returns the value of the model's current position. * - * @return Eloquent Query Builder instance + * @return int */ - public function scopeListifyScope($query) + public function getListifyPosition() { - return $query->whereRaw($this->scopeCondition()); + return $this->getAttribute($this->getPositionColumnName()); } /** - * An Eloquent scope that returns only items currently in the list. + * Get the name of the position 'column' option. * - * @param $query + * @return string + */ + public function getPositionColumnName() + { + return $this->getListifyConfig()->getPositionColumnName(); + } + + /** + * Return the next higher item in the list. * - * @return Eloquent Query Builder instance + * @return null|static */ - public function scopeInList($query) + public function higherItem() { - return $query->listifyScope()->whereNotNull($this->getTable().'.'.$this->getPositionColumnName()); + if ($this->isNotInList()) { + return; + } + + return $this->listifyList() + ->where($this->getPositionColumnName(), '<', $this->getListifyPosition()) + ->orderBy($this->getTable().'.'.$this->getPositionColumnName(), 'DESC') + ->first(); } /** - * Get the value of the top of list option. + * Return the next n higher items in the list. Selects all higher items by default. * - * @return string + * @param null|int $limit The maximum number of items to return + * + * @return \Illuminate\Database\Eloquent\Collection|static[] */ - public function listifyTopPositionInList() + public function higherItems($limit = null) { - return $this->getListifyConfig()->getTopPositionInList(); + if ($limit === null) { + $limit = $this->listifyList()->count(); + } + + $positionValue = $this->getListifyPosition(); + + return $this->listifyList() + ->where($this->getPositionColumnName(), '<', $positionValue) + ->where($this->getPositionColumnName(), '>=', $positionValue - $limit) + ->orderBy($this->getTable().'.'.$this->getPositionColumnName(), 'ASC') + ->take($limit) + ->get(); } /** - * Get the name of the position 'column' option. + * Increase the position of this item without adjusting the rest of the list. * - * @return string + * @param int $count default 1 + * + * @return $this */ - public function getPositionColumnName() + public function incrementPosition($count = 1) { - return $this->getListifyConfig()->getPositionColumnName(); + if ($this->isNotInList()) { + return $this; + } + + $this->setListifyPosition($this->getListifyPosition() + $count); + + return $this; } /** - * Get the value of the 'scope' option. + * Insert the item at the given position (defaults to the top position of 1). * - * @return mixed Can be a string, an Eloquent BelongsTo, or an Eloquent Builder + * @param int $position + * + * @return $this */ - public function scopeName() + public function insertAt($position = null) { - return $this->getListifyConfig()->getScope(); + if ($position === null) { + $position = $this->listifyTopPositionInList(); + } + + $this->insertAtPosition($position); + + return $this; } /** - * Returns the value of the 'add_new_at' option. + * Returns if this object is the first in the list. * - * @return string + * @return bool */ - public function addNewAt() + public function isFirst() { - return $this->getListifyConfig()->getAddNewItemTo(); + if ($this->isNotInList()) { + return false; + } + + return $this->getListifyPosition() == $this->listifyTopPositionInList(); } /** - * Returns the value of the model's current position. + * Returns whether the item is in the list. * - * @return int + * @return bool */ - public function getListifyPosition() + public function isInList() { - return $this->getAttribute($this->getPositionColumnName()); + return $this->getListifyPosition() !== null; } /** - * Sets the value of the model's position. + * Returns if this object is the last in the list. * - * @param int $position + * @return bool + */ + public function isLast() + { + if ($this->isNotInList()) { + return false; + } + + return $this->getListifyPosition() == $this->bottomPositionInList(); + } + + /** + * Returns whether the item is not in the list. * - * @return void + * @return bool */ - public function setListifyPosition($position) + public function isNotInList() { - $this->setAttribute($this->getPositionColumnName(), $position); + return ! $this->isInList(); } /** - * Insert the item at the given position (defaults to the top position of 1). + * Get the value of the top of list option. * - * @param int $position + * @return string + */ + public function listifyTopPositionInList() + { + return $this->getListifyConfig()->getTopPositionInList(); + } + + /** + * Return the next lower item in the list. * - * @return $this + * @return null|static */ - public function insertAt($position = null) + public function lowerItem() { - if ($position === null) { - $position = $this->listifyTopPositionInList(); + if ($this->isNotInList()) { + return; } - $this->insertAtPosition($position); + return $this->listifyList() + ->where($this->getPositionColumnName(), '>', $this->getListifyPosition()) + ->orderBy($this->getTable().'.'.$this->getPositionColumnName(), 'ASC') + ->first(); + } - return $this; + /** + * Return the next n lower items in the list. Selects all lower items by default. + * + * @param null|int $limit The maximum number of items to return + * + * @return \Illuminate\Database\Eloquent\Collection|static[] + */ + public function lowerItems($limit = null) + { + $query = $this->listifyList() + ->where($this->getPositionColumnName(), '>', $this->getListifyPosition()) + ->orderBy($this->getTable().'.'.$this->getPositionColumnName(), 'ASC'); + + if ($limit !== null) { + $query->take($limit); + } + + return $query->get(); } /** - * Swap positions with the next lower item, if one exists. + * Swap positions with the next higher item, if one exists. * * @return $this */ - public function moveLower() + public function moveHigher() { - if (! $this->lowerItem()) { + if (! $this->higherItem()) { return $this; } - $this->getConnection()->transaction(function () { - $this->lowerItem()->decrement($this->getPositionColumnName()); - $this->increment($this->getPositionColumnName()); + $this->getConnection()->transaction(function (): void { + $this->higherItem()->increment($this->getPositionColumnName()); + $this->decrement($this->getPositionColumnName()); }); return $this; } /** - * Swap positions with the next higher item, if one exists. + * Swap positions with the next lower item, if one exists. * * @return $this */ - public function moveHigher() + public function moveLower() { - if (! $this->higherItem()) { + if (! $this->lowerItem()) { return $this; } - $this->getConnection()->transaction(function () { - $this->higherItem()->increment($this->getPositionColumnName()); - $this->decrement($this->getPositionColumnName()); + $this->getConnection()->transaction(function (): void { + $this->lowerItem()->decrement($this->getPositionColumnName()); + $this->increment($this->getPositionColumnName()); }); return $this; @@ -253,7 +329,7 @@ public function moveToBottom() return $this; } - $this->getConnection()->transaction(function () { + $this->getConnection()->transaction(function (): void { $this->decrementPositionsOnLowerItems(); $this->setListPosition($this->bottomPositionInList($this) + 1); }); @@ -272,7 +348,7 @@ public function moveToTop() return $this; } - $this->getConnection()->transaction(function () { + $this->getConnection()->transaction(function (): void { $this->incrementPositionsOnHigherItems(); $this->setListPosition($this->listifyTopPositionInList()); }); @@ -296,164 +372,49 @@ public function removeFromList() } /** - * Increase the position of this item without adjusting the rest of the list. + * An Eloquent scope that returns only items currently in the list. * - * @param int $count default 1 + * @param $query * - * @return $this + * @return Eloquent Query Builder instance */ - public function incrementPosition($count = 1) + public function scopeInList($query) { - if ($this->isNotInList()) { - return $this; - } - - $this->setListifyPosition($this->getListifyPosition() + $count); - - return $this; - } - - /** - * Decrease the position of this item without adjusting the rest of the list. - * - * @param int $count default 1 - * - * @return $this - */ - public function decrementPosition($count = 1) - { - if ($this->isNotInList()) { - return $this; - } - - $this->setListifyPosition($this->getListifyPosition() - $count); - - return $this; - } - - /** - * Returns if this object is the first in the list. - * - * @return bool - */ - public function isFirst() - { - if ($this->isNotInList()) { - return false; - } - - return $this->getListifyPosition() == $this->listifyTopPositionInList(); - } + return $query->listifyScope()->whereNotNull($this->getTable().'.'.$this->getPositionColumnName()); + } /** - * Returns if this object is the last in the list. - * - * @return bool - */ - public function isLast() - { - if ($this->isNotInList()) { - return false; - } - - return $this->getListifyPosition() == $this->bottomPositionInList(); - } - - /** - * Return the next higher item in the list. - * - * @return null|static - */ - public function higherItem() - { - if ($this->isNotInList()) { - return; - } - - return $this->listifyList() - ->where($this->getPositionColumnName(), '<', $this->getListifyPosition()) - ->orderBy($this->getTable().'.'.$this->getPositionColumnName(), 'DESC') - ->first(); - } - - /** - * Return the next n higher items in the list. Selects all higher items by default. - * - * @param null|int $limit The maximum number of items to return + * An Eloquent scope based on the processed scope option. * - * @return \Illuminate\Database\Eloquent\Collection|static[] - */ - public function higherItems($limit = null) - { - if ($limit === null) { - $limit = $this->listifyList()->count(); - } - - $positionValue = $this->getListifyPosition(); - - return $this->listifyList() - ->where($this->getPositionColumnName(), '<', $positionValue) - ->where($this->getPositionColumnName(), '>=', $positionValue - $limit) - ->orderBy($this->getTable().'.'.$this->getPositionColumnName(), 'ASC') - ->take($limit) - ->get(); - } - - /** - * Return the next lower item in the list. + * @param $query An Eloquent Query Builder instance * - * @return null|static + * @return Eloquent Query Builder instance */ - public function lowerItem() + public function scopeListifyScope($query) { - if ($this->isNotInList()) { - return; - } - - return $this->listifyList() - ->where($this->getPositionColumnName(), '>', $this->getListifyPosition()) - ->orderBy($this->getTable().'.'.$this->getPositionColumnName(), 'ASC') - ->first(); + return $query->whereRaw($this->scopeCondition()); } /** - * Return the next n lower items in the list. Selects all lower items by default. - * - * @param null|int $limit The maximum number of items to return + * Get the value of the 'scope' option. * - * @return \Illuminate\Database\Eloquent\Collection|static[] + * @return mixed Can be a string, an Eloquent BelongsTo, or an Eloquent Builder */ - public function lowerItems($limit = null) + public function scopeName() { - $query = $this->listifyList() - ->where($this->getPositionColumnName(), '>', $this->getListifyPosition()) - ->orderBy($this->getTable().'.'.$this->getPositionColumnName(), 'ASC'); - - if ($limit !== null) { - $query->take($limit); - } - - return $query->get(); + return $this->getListifyConfig()->getScope(); } /** - * Returns whether the item is in the list. + * Sets the value of the model's position. * - * @return bool - */ - public function isInList() - { - return $this->getListifyPosition() !== null; - } - - /** - * Returns whether the item is not in the list. + * @param int $position * - * @return bool + * @return void */ - public function isNotInList() + public function setListifyPosition($position): void { - return ! $this->isInList(); + $this->setAttribute($this->getPositionColumnName(), $position); } /** @@ -471,45 +432,69 @@ public function setListPosition($position = null) } /** - * Creates an instance of the current class scope as a list. + * Returns whether the scope has changed during the course of interaction with the model. * - * @return mixed + * @return bool */ - protected function listifyList() + public static function bootListify() { - $model = new self(); - $model->getListifyConfig()->setScope($this->scopeCondition()); + //Bind to model events + static::deleting(function ($model): void { + /* @var Listify $model */ + $model->reloadPosition(); + }); - return $model->listifyScope(); + static::deleted(function ($model): void { + /* @var Listify $model */ + $model->decrementPositionsOnLowerItems(); + }); + + static::updating(function ($model): void { + /* @var Listify $model */ + $model->checkScope(); + }); + + static::updated(function ($model): void { + /* @var Listify $model */ + $model->updatePositions(); + }); + + static::creating(function ($model): void { + /* @var Listify $model */ + if ($model->addNewAt()) { + $methodName = 'addToList'.$model->addNewAt(); + $model->{$methodName}(); + } + }); } /** - * Adds item to the top of the list. + * Adds item to the bottom of the list. * * @return void */ - protected function addToListTop() + protected function addToListBottom(): void { if ($this->isInList()) { return; } - $this->incrementPositionsOnAllItems(); - $this->setListifyPosition($this->listifyTopPositionInList()); + $this->setListifyPosition($this->bottomPositionInList() + 1); } /** - * Adds item to the bottom of the list. + * Adds item to the top of the list. * * @return void */ - protected function addToListBottom() + protected function addToListTop(): void { if ($this->isInList()) { return; } - $this->setListifyPosition($this->bottomPositionInList() + 1); + $this->incrementPositionsOnAllItems(); + $this->setListifyPosition($this->listifyTopPositionInList()); } /** @@ -525,9 +510,52 @@ protected function bottomPositionInList($except = null) if ($item) { return $item->getListifyPosition(); - } else { - return $this->listifyTopPositionInList() - 1; } + + return $this->listifyTopPositionInList() - 1; + } + + /** + * Determines whether scope has changed. If so, it will move the current item to the top/bottom of the list and update all other items. + * + * @return void + */ + protected function checkScope(): void + { + if ($this->hasScopeChanged()) { + $this->swapChangedAttributes(); + if ($this->lowerItem()) { + $this->decrementPositionsOnLowerItems(); + } + + $this->swapChangedAttributes(); + // make this item "not in the list" so subsequent call to addToListBottom() works (b/c it only operates on items that have no position) + $this->setListifyPosition(null); + $methodName = 'addToList'.$this->addNewAt(); + $this->{$methodName}(); + } + } + + /** + * This has the effect of moving all the lower items up one. + * + * @param int $position All items below the passed in position will be modified + * + * @return void + */ + protected function decrementPositionsOnLowerItems($position = null): void + { + if ($this->isNotInList()) { + return; + } + + if ($position === null) { + $position = $this->getListifyPosition(); + } + + $this->listifyList() + ->where($this->getPositionColumnName(), '>', $position) + ->decrement($this->getPositionColumnName()); } /** @@ -545,14 +573,56 @@ protected function getBottomItem($except = null) $conditions = $conditions.' AND '.$this->getPrimaryKey().' != '.$except->id; } - $list = $this->listifyList() + return $this->listifyList() ->whereNotNull($this->getTable().'.'.$this->getPositionColumnName()) ->whereRaw($conditions) ->orderBy($this->getTable().'.'.$this->getPositionColumnName(), 'DESC') ->take(1) ->first(); + } - return $list; + /** + * Returns a raw WHERE clause based off of a Query Builder object. + * + * @param $query A Query Builder instance + * + * @return string + */ + protected function getConditionStringFromQueryBuilder($query) + { + $initialQueryChunks = explode('where ', $query->toSql()); + if (count($initialQueryChunks) == 1) { + throw new InvalidQueryBuilderException('The Listify scope is a Query Builder object, but it has no "where", so it can\'t be used as a scope.'); + } + + $queryChunks = explode('?', $initialQueryChunks[1]); + $bindings = $query->getBindings(); + + $theQuery = ''; + + for ($i = 0; $i < count($queryChunks); $i++) { + // "boolean" + // "integer" + // "double" (for historical reasons "double" is returned in case of a float, and not simply "float") + // "string" + // "array" + // "object" + // "resource" + // "NULL" + // "unknown type" + + $theQuery .= $queryChunks[$i]; + if (isset($bindings[$i])) { + switch (gettype($bindings[$i])) { + case 'string': + $theQuery .= '\''.$bindings[$i].'\''; + + break; + } + } + } + + return $theQuery; } /** @@ -566,25 +636,58 @@ protected function getPrimaryKey() } /** - * This has the effect of moving all the lower items up one. - * - * @param int $position All items below the passed in position will be modified + * Returns whether the scope has changed during the course of interaction with the model. * - * @return void + * @return bool */ - protected function decrementPositionsOnLowerItems($position = null) + protected function hasScopeChanged() { - if ($this->isNotInList()) { - return; + $theScope = $this->scopeName(); + + if (is_string($theScope)) { + if (! $this->stringScopeValue) { + $this->stringScopeValue = $theScope; + + return false; + } + + return $theScope != $this->stringScopeValue; } - if ($position === null) { - $position = $this->getListifyPosition(); + $reflector = new \ReflectionClass($theScope); + if ($reflector->getName() == 'Illuminate\Database\Eloquent\Relations\BelongsTo') { + $foreignKey = method_exists($theScope, 'getForeignKey') ? $theScope->getForeignKey() : $theScope->getForeignKeyName(); + $originalVal = $this->getOriginal()[$foreignKey]; + $currentVal = $this->getAttribute($foreignKey); + + if ($originalVal != $currentVal) { + return true; + } + } elseif ($reflector->getName() == 'Illuminate\Database\Query\Builder') { + if (! $this->stringScopeValue) { + $this->stringScopeValue = $this->getConditionStringFromQueryBuilder($theScope); + + return false; + } + + $theQuery = $this->getConditionStringFromQueryBuilder($theScope); + if ($theQuery != $this->stringScopeValue) { + return true; + } } + return false; + } + + /** + * Increments position of all items in the list. + * + * @return void + */ + protected function incrementPositionsOnAllItems(): void + { $this->listifyList() - ->where($this->getPositionColumnName(), '>', $position) - ->decrement($this->getPositionColumnName()); + ->increment($this->getPositionColumnName()); } /** @@ -592,7 +695,7 @@ protected function decrementPositionsOnLowerItems($position = null) * * @return void */ - protected function incrementPositionsOnHigherItems() + protected function incrementPositionsOnHigherItems(): void { if ($this->isNotInList()) { return; @@ -610,66 +713,13 @@ protected function incrementPositionsOnHigherItems() * * @return void */ - protected function incrementPositionsOnLowerItems($position) + protected function incrementPositionsOnLowerItems($position): void { $this->listifyList() ->where($this->getPositionColumnName(), '>=', $position) ->increment($this->getPositionColumnName()); } - /** - * Increments position of all items in the list. - * - * @return void - */ - protected function incrementPositionsOnAllItems() - { - $this->listifyList() - ->increment($this->getPositionColumnName()); - } - - /** - * Reorders intermediate items to support moving an item from oldPosition to newPosition. - * - * @param int $oldPosition - * @param int $newPosition - * @param string $avoidId You can pass in an ID of a record matching the current class and it will be ignored - * - * @return void - */ - protected function shufflePositionsOnIntermediateItems($oldPosition, $newPosition, $avoidId = null) - { - if ($oldPosition == $newPosition) { - return; - } - - $avoidIdCondition = $avoidId ? $this->getPrimaryKey().' != '.$avoidId : '1 = 1'; - - if ($oldPosition < $newPosition) { - // Decrement position of intermediate items - - // e.g., if moving an item from 2 to 5, - // move [3, 4, 5] to [2, 3, 4] - - $this->listifyList() - ->where($this->getPositionColumnName(), '>', $oldPosition) - ->where($this->getPositionColumnName(), '<=', $newPosition) - ->whereRaw($avoidIdCondition) - ->decrement($this->getPositionColumnName()); - } else { - // Increment position of intermediate items - - // e.g., if moving an item from 5 to 2, - // move [2, 3, 4] to [3, 4, 5] - - $this->listifyList() - ->where($this->getPositionColumnName(), '>=', $newPosition) - ->where($this->getPositionColumnName(), '<', $oldPosition) - ->whereRaw($avoidIdCondition) - ->increment($this->getPositionColumnName()); - } - } - /** * Inserts the item at a particular location in the list. All items around it will be modified. * @@ -677,7 +727,7 @@ protected function shufflePositionsOnIntermediateItems($oldPosition, $newPositio * * @return void */ - protected function insertAtPosition($position) + protected function insertAtPosition($position): void { if ($this->isInList()) { $oldPosition = $this->getListifyPosition(); @@ -694,68 +744,16 @@ protected function insertAtPosition($position) } /** - * Updates all items based on the original position of the item and the new position of the item. - * - * @return void - */ - protected function updatePositions() - { - $oldPosition = $this->getOriginal()[$this->getPositionColumnName()]; - $newPosition = $this->getListifyPosition(); - - if ($newPosition === null) { - $matchingPositionRecords = 0; - } else { - $matchingPositionRecords = $this->listifyList()->where($this->getPositionColumnName(), '=', $newPosition)->count(); - } - - if ($matchingPositionRecords <= 1) { - return; - } - - $this->shufflePositionsOnIntermediateItems($oldPosition, $newPosition, $this->id); - } - - /** - * Temporarily swap changes attributes with current attributes. - * - * @return void - */ - protected function swapChangedAttributes() - { - if ($this->originalAttributesLoaded === false) { - $this->swappedAttributes = $this->getAttributes(); - $this->fill($this->getOriginal()); - $this->originalAttributesLoaded = true; - } else { - if (count($this->swappedAttributes) == 0) { - $this->swappedAttributes = $this->getAttributes(); - } - - $this->fill($this->swappedAttributes); - $this->originalAttributesLoaded = false; - } - } - - /** - * Determines whether scope has changed. If so, it will move the current item to the top/bottom of the list and update all other items. + * Creates an instance of the current class scope as a list. * - * @return void + * @return mixed */ - protected function checkScope() + protected function listifyList() { - if ($this->hasScopeChanged()) { - $this->swapChangedAttributes(); - if ($this->lowerItem()) { - $this->decrementPositionsOnLowerItems(); - } + $model = new self(); + $model->getListifyConfig()->setScope($this->scopeCondition()); - $this->swapChangedAttributes(); - // make this item "not in the list" so subsequent call to addToListBottom() works (b/c it only operates on items that have no position) - $this->setListifyPosition(null); - $methodName = 'addToList'.$this->addNewAt(); - $this->$methodName(); - } + return $model->listifyScope(); } /** @@ -763,55 +761,11 @@ protected function checkScope() * * @return void */ - protected function reloadPosition() + protected function reloadPosition(): void { $this->setListifyPosition($this->getOriginal()[$this->getPositionColumnName()]); } - /** - * Returns whether the scope has changed during the course of interaction with the model. - * - * @return bool - */ - protected function hasScopeChanged() - { - $theScope = $this->scopeName(); - - if (is_string($theScope)) { - if (! $this->stringScopeValue) { - $this->stringScopeValue = $theScope; - - return false; - } - - return $theScope != $this->stringScopeValue; - } - - $reflector = new \ReflectionClass($theScope); - if ($reflector->getName() == 'Illuminate\Database\Eloquent\Relations\BelongsTo') { - $foreignKey = method_exists($theScope, 'getForeignKey') ? $theScope->getForeignKey() : $theScope->getForeignKeyName(); - $originalVal = $this->getOriginal()[$foreignKey]; - $currentVal = $this->getAttribute($foreignKey); - - if ($originalVal != $currentVal) { - return true; - } - } elseif ($reflector->getName() == 'Illuminate\Database\Query\Builder') { - if (! $this->stringScopeValue) { - $this->stringScopeValue = $this->getConditionStringFromQueryBuilder($theScope); - - return false; - } - - $theQuery = $this->getConditionStringFromQueryBuilder($theScope); - if ($theQuery != $this->stringScopeValue) { - return true; - } - } - - return false; - } - /** * Returns the raw WHERE clause to be used as the Listify scope. * @@ -839,9 +793,8 @@ protected function scopeCondition() if ($relationshipId === null) { throw new NullForeignKeyException('The Listify scope is a "belongsTo" relationship, but the foreign key is null.'); - } else { - $theScope = $foreignKey.' = '.$this->getAttribute($foreignKey); } + $theScope = $foreignKey.' = '.$this->getAttribute($foreignKey); } elseif ($reflector->getName() == 'Illuminate\Database\Query\Builder') { $this->stringScopeValue = $theScope = $this->getConditionStringFromQueryBuilder($theScope); } else { @@ -857,45 +810,88 @@ protected function scopeCondition() } /** - * Returns a raw WHERE clause based off of a Query Builder object. + * Reorders intermediate items to support moving an item from oldPosition to newPosition. * - * @param $query A Query Builder instance + * @param int $oldPosition + * @param int $newPosition + * @param string $avoidId You can pass in an ID of a record matching the current class and it will be ignored * - * @return string + * @return void */ - protected function getConditionStringFromQueryBuilder($query) + protected function shufflePositionsOnIntermediateItems($oldPosition, $newPosition, $avoidId = null): void { - $initialQueryChunks = explode('where ', $query->toSql()); - if (count($initialQueryChunks) == 1) { - throw new InvalidQueryBuilderException('The Listify scope is a Query Builder object, but it has no "where", so it can\'t be used as a scope.'); + if ($oldPosition == $newPosition) { + return; } - $queryChunks = explode('?', $initialQueryChunks[1]); - $bindings = $query->getBindings(); + $avoidIdCondition = $avoidId ? $this->getPrimaryKey().' != '.$avoidId : '1 = 1'; - $theQuery = ''; + if ($oldPosition < $newPosition) { + // Decrement position of intermediate items - for ($i = 0; $i < count($queryChunks); $i++) { - // "boolean" - // "integer" - // "double" (for historical reasons "double" is returned in case of a float, and not simply "float") - // "string" - // "array" - // "object" - // "resource" - // "NULL" - // "unknown type" + // e.g., if moving an item from 2 to 5, + // move [3, 4, 5] to [2, 3, 4] - $theQuery .= $queryChunks[$i]; - if (isset($bindings[$i])) { - switch (gettype($bindings[$i])) { - case 'string': - $theQuery .= '\''.$bindings[$i].'\''; - break; - } + $this->listifyList() + ->where($this->getPositionColumnName(), '>', $oldPosition) + ->where($this->getPositionColumnName(), '<=', $newPosition) + ->whereRaw($avoidIdCondition) + ->decrement($this->getPositionColumnName()); + } else { + // Increment position of intermediate items + + // e.g., if moving an item from 5 to 2, + // move [2, 3, 4] to [3, 4, 5] + + $this->listifyList() + ->where($this->getPositionColumnName(), '>=', $newPosition) + ->where($this->getPositionColumnName(), '<', $oldPosition) + ->whereRaw($avoidIdCondition) + ->increment($this->getPositionColumnName()); + } + } + + /** + * Temporarily swap changes attributes with current attributes. + * + * @return void + */ + protected function swapChangedAttributes(): void + { + if ($this->originalAttributesLoaded === false) { + $this->swappedAttributes = $this->getAttributes(); + $this->fill($this->getOriginal()); + $this->originalAttributesLoaded = true; + } else { + if (count($this->swappedAttributes) == 0) { + $this->swappedAttributes = $this->getAttributes(); } + + $this->fill($this->swappedAttributes); + $this->originalAttributesLoaded = false; } + } - return $theQuery; + /** + * Updates all items based on the original position of the item and the new position of the item. + * + * @return void + */ + protected function updatePositions(): void + { + $oldPosition = $this->getOriginal()[$this->getPositionColumnName()]; + $newPosition = $this->getListifyPosition(); + + if ($newPosition === null) { + $matchingPositionRecords = 0; + } else { + $matchingPositionRecords = $this->listifyList()->where($this->getPositionColumnName(), '=', $newPosition)->count(); + } + + if ($matchingPositionRecords <= 1) { + return; + } + + $this->shufflePositionsOnIntermediateItems($oldPosition, $newPosition, $this->id); } } diff --git a/src/ListifyServiceProvider.php b/src/ListifyServiceProvider.php index 8d17ea2..a5363c5 100644 --- a/src/ListifyServiceProvider.php +++ b/src/ListifyServiceProvider.php @@ -12,10 +12,8 @@ class ListifyServiceProvider extends ServiceProvider * * @return void */ - public function boot() + public function boot(): void { - $this->loadViewsFrom(__DIR__.'/../../views', 'listify'); - if ($this->app->runningInConsole()) { $this->commands([ AttachCommand::class, @@ -28,7 +26,7 @@ public function boot() * * @return void */ - public function register() + public function register(): void { } }