Skip to content

Commit 17b2c9b

Browse files
author
Martin Kluska
committed
Initial commit
1 parent 6c038d9 commit 17b2c9b

14 files changed

+1076
-0
lines changed

.gitignore

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Created by .ignore support plugin (hsz.mobi)
2+
### Composer template
3+
composer.phar
4+
/vendor/
5+
6+
# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
7+
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
8+
composer.lock
9+
### Laravel template
10+
vendor/
11+
node_modules/
12+
13+
# Laravel 4 specific
14+
bootstrap/compiled.php
15+
app/storage/
16+
17+
# Laravel 5 & Lumen specific
18+
public/storage
19+
storage/*.key
20+
.env.*.php
21+
.env.php
22+
.env
23+
24+
# Rocketeer PHP task runner and deployment package. https://github.com/rocketeers/rocketeer
25+
.rocketeer/

CONTRIBUTING.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Contribution or overriding
2+
Are welcome. To add a new provider just add a new Handler (which extends AbstractHandler). Then implement the chunk
3+
upload and progress.
4+
5+
1. Fork the project.
6+
2. Create your bugfix/feature branch and write your (try well-commented) code.
7+
3. Commit your changes (and your tests) and push to your branch.
8+
4. Create a new pull request against this package's `master` branch.
9+
10+
## Pull Requests
11+
12+
- **Use the [PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md).**
13+
The easiest way to apply the conventions is to install [PHP Code Sniffer](http://pear.php.net/package/PHP_CodeSniffer) or install dev depedency and run the `composer run-script lint` or `composer run-script lint-fix`
14+
15+
- **Consider our release cycle.** We try to follow [SemVer v2.0.0](http://semver.org/).
16+
17+
- **Document any change in behaviour.** Make sure the `README.md` and any other relevant
18+
documentation are kept up-to-date.
19+
20+
- **Create feature branches.** Don't ask us to pull from your master branch.
21+
22+
- **One pull request per feature.** If you want to do more than one thing, send multiple pull requests.
23+
24+
**Thank you!**

LICENSE.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# The MIT License (MIT)
2+
3+
Copyright (c) 2016 Martin Kluska <martin@kluska.cz>
4+
5+
> Permission is hereby granted, free of charge, to any person obtaining a copy
6+
> of this software and associated documentation files (the "Software"), to deal
7+
> in the Software without restriction, including without limitation the rights
8+
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
> copies of the Software, and to permit persons to whom the Software is
10+
> furnished to do so, subject to the following conditions:
11+
>
12+
> The above copyright notice and this permission notice shall be included in all
13+
> copies or substantial portions of the Software.
14+
>
15+
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
> SOFTWARE.

composer.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "pion/laravel-eloquent-position",
3+
"description": "Position logic for Eloquent models with minimum setup",
4+
"require": {
5+
"laravel/framework": "5.1.* || 5.2.* || 5.3.*"
6+
},
7+
"require-dev": {
8+
"squizlabs/php_codesniffer": "2.*"
9+
},
10+
"scripts": {
11+
"lint": "./vendor/bin/phpcs --standard=PSR2 src/",
12+
"lint-fix": "./vendor/bin/phpcbf --standard=PSR2 src/"
13+
},
14+
"autoload": {
15+
"psr-4": {
16+
"Pion\\Support\\Eloquent\\Position\\": "src/"
17+
}
18+
},
19+
"license": "MIT",
20+
"authors": [
21+
{
22+
"name": "Martin Kluska",
23+
"email": "martin.kluska@imakers.cz"
24+
}
25+
],
26+
"minimum-stability": "stable"
27+
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<?php
2+
3+
namespace App\Console\Commands;
4+
5+
use Pion\Support\Eloquent\Position\Traits\PositionTrait;
6+
use Illuminate\Console\Command;
7+
use Illuminate\Database\Eloquent\Collection;
8+
use Illuminate\Database\Eloquent\Model;
9+
10+
class RecalculatePositionCommand extends Command
11+
{
12+
/**
13+
* The name and signature of the console command.
14+
*
15+
* @var string
16+
*/
17+
protected $signature = 'model:position {model} ';
18+
19+
/**
20+
* The console command description.
21+
*
22+
* @var string
23+
*/
24+
protected $description = 'Recalculates given model position. You must provide full model class';
25+
26+
/**
27+
* Create a new command instance.
28+
*/
29+
public function __construct()
30+
{
31+
parent::__construct();
32+
}
33+
34+
/**
35+
* Execute the console command.
36+
*
37+
* @return mixed
38+
*/
39+
public function handle()
40+
{
41+
// Get the model
42+
/** @var PositionTrait $modelClass */
43+
$modelClass = $this->argument('model');
44+
45+
//
46+
/**
47+
* Build the model instance so we can get the settings
48+
* @var PositionTrait $model
49+
*/
50+
$model = new $modelClass();
51+
52+
// Check if using the PositionTrait
53+
if (!method_exists($model, 'newPositionQuery')) {
54+
$this->error("Model {$modelClass} is not using PositionTrait");
55+
return false;
56+
}
57+
58+
// Holds the positions for every group (defined by the values
59+
$positionsByGroup = [];
60+
61+
// Get the group
62+
$groups = $model->getPositionGroup();
63+
64+
// Run sorted query for every entry
65+
$modelClass::sorted()->chunk(200, function (Collection $collection) use ($groups, &$positionsByGroup) {
66+
/** @var PositionTrait|Model $model */
67+
foreach ($collection as $model) {
68+
// Builds the group key to get position
69+
$groupKey = $this->buildGroupKeyForPosition($model, $groups);
70+
71+
// Set the new position
72+
$model->setPosition($this->getPositionForGroup($groupKey, $positionsByGroup))
73+
->save();
74+
}
75+
});
76+
77+
// Render the table layout about the positions
78+
$this->table([
79+
'Group', 'Last position'
80+
], collect($positionsByGroup)->map(function ($value, $key) {
81+
return [$key, $value];
82+
}));
83+
84+
return true;
85+
}
86+
87+
/**
88+
* Stores/updates the next position
89+
*
90+
* @param string $groupKey
91+
* @param array $positionsByGroup referenced array of currently stores positions
92+
*
93+
* @return mixed
94+
*/
95+
protected function getPositionForGroup($groupKey, &$positionsByGroup)
96+
{
97+
if (!isset($positionsByGroup[$groupKey])) {
98+
$positionsByGroup[$groupKey] = 0;
99+
}
100+
// Increment the position
101+
$positionsByGroup[$groupKey]++;
102+
103+
// Return the new position
104+
return $positionsByGroup[$groupKey];
105+
}
106+
107+
/**
108+
* Builds the group key from the group columns and the values form the model
109+
*
110+
* @param Model|PositionTrait $model
111+
* @param array $groups
112+
*
113+
* @return string
114+
*/
115+
protected function buildGroupKeyForPosition($model, $groups)
116+
{
117+
if (is_null($groups)) {
118+
return 'No group';
119+
}
120+
121+
// Prepare array of values to create single string
122+
$groupValues = [];
123+
124+
// Get the group column values
125+
foreach ($groups as $column) {
126+
$groupValues[] = $model->{$column};
127+
}
128+
129+
// Build the key
130+
return implode('_', $groupValues);
131+
}
132+
}

src/PositionObserver.php

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
<?php
2+
namespace Pion\Support\Eloquent\Position;
3+
4+
use Pion\Support\Eloquent\Position\Query\AbstractPositionQuery;
5+
use Pion\Support\Eloquent\Position\Query\LastPositionQuery;
6+
use Pion\Support\Eloquent\Position\Query\PositionQuery;
7+
use Pion\Support\Eloquent\Position\Traits\PositionTrait;
8+
use Illuminate\Database\Eloquent\Model;
9+
use Illuminate\Contracts\Events\Dispatcher;
10+
11+
/**
12+
* Traits PositionObserver
13+
*
14+
* Listens for saved state and checks if the position should be update in
15+
* related entries.
16+
*
17+
* @package App\Models\Traits
18+
*/
19+
class PositionObserver
20+
{
21+
/**
22+
* @var Dispatcher
23+
*/
24+
private $events;
25+
26+
/**
27+
* PositionObserver constructor.
28+
*
29+
* @param Dispatcher $events
30+
*/
31+
public function __construct(Dispatcher $events)
32+
{
33+
$this->events = $events;
34+
}
35+
36+
/**
37+
* Updates the position before saving
38+
*
39+
* @param Model|PositionTrait $model
40+
*/
41+
public function saving($model)
42+
{
43+
if (!$model->isPositionUpdateDisabled()) {
44+
// Get the position for current and old value
45+
$position = $model->getPosition();
46+
47+
// Get the old position
48+
$oldPosition = $model->getOriginal($model->getPositionColumn());
49+
50+
// Check if the position is set
51+
if (is_null($position)) {
52+
$this->appendLast($model, $oldPosition);
53+
} else {
54+
$this->move($model, $position, $oldPosition);
55+
}
56+
}
57+
}
58+
59+
/**
60+
* Setups the position to be at the end
61+
*
62+
* @param Model|PositionTrait $model
63+
* @param int $oldPosition
64+
*/
65+
protected function appendLast($model, $oldPosition)
66+
{
67+
// Build the last position query
68+
$query = new LastPositionQuery($model, $oldPosition);
69+
70+
// Run the query
71+
$this->runQuery($query, $oldPosition);
72+
}
73+
74+
/**
75+
* Moves other entries from the given position
76+
*
77+
* @param Model|PositionTrait $model
78+
* @param int $position
79+
* @param int $oldPosition
80+
*/
81+
protected function move($model, $position, $oldPosition)
82+
{
83+
// Check if the position has change and we need to recalculate
84+
if ($oldPosition != $position) {
85+
// Build the position query
86+
$query = new PositionQuery($model, $position, $oldPosition);
87+
88+
// Run the position query
89+
$this->runQuery($query, $oldPosition);
90+
}
91+
}
92+
93+
/**
94+
* Runs the position events and query if can. If positioning event returns
95+
* false, it will revert the new position to old position.
96+
*
97+
* @param AbstractPositionQuery $query
98+
* @param int $oldPosition
99+
*/
100+
protected function runQuery(AbstractPositionQuery $query, $oldPosition)
101+
{
102+
// Fire the validation event
103+
$eventResponse = $this->firePositioningEvent($query);
104+
105+
// Ignore updating the position and revert the position to original value
106+
if ($eventResponse !== $eventResponse && $eventResponse === false) {
107+
// Update the new position to original position
108+
$query->model()->setPosition($oldPosition);
109+
} else {
110+
// Run the query to update other entries to fix the position order
111+
$query->run();
112+
113+
// Fire the final state
114+
$this->firePositionedEvent($query->model());
115+
}
116+
}
117+
118+
/**
119+
* Fire the name-spaced validating event.
120+
*
121+
* @param AbstractPositionQuery $query
122+
*
123+
* @return mixed
124+
*/
125+
protected function firePositioningEvent($query)
126+
{
127+
$model = $query->model();
128+
return $this->events->until('eloquent.positioning: '.get_class($model), [$model, $query]);
129+
}
130+
131+
/**
132+
* Fire the name-spaced post-validation event.
133+
*
134+
* @param Model $model
135+
*
136+
* @return void
137+
*/
138+
protected function firePositionedEvent(Model $model)
139+
{
140+
$this->events->fire('eloquent.positioned: '.get_class($model), [$model]);
141+
}
142+
}

0 commit comments

Comments
 (0)