├── README.md ├── composer.json └── src ├── ArgumentBag.php ├── Builder.php ├── Contracts ├── ArgumentBag.php └── Pipeline.php ├── Hookable.php └── Pipeline.php /README.md: -------------------------------------------------------------------------------- 1 | # Sofa/Hookable 2 | 3 | [![GitHub Tests Action Status](https://github.com/jarektkaczyk/hookable/workflows/Tests/badge.svg)](https://github.com/jarektkaczyk/hookable/actions?query=workflow%3Atests+branch%3Amaster) [![stable](https://poser.pugx.org/sofa/hookable/v/stable.svg)](https://packagist.org/packages/sofa/hookable) [![Downloads](https://poser.pugx.org/sofa/hookable/downloads)](https://packagist.org/packages/sofa/hookable) 4 | 5 | Hooks system for the [Eloquent ORM (Laravel 5.2)](https://laravel.com/docs/5.2/eloquent). 6 | 7 | Hooks are available for the following methods: 8 | 9 | * `Model::getAttribute` 10 | * `Model::setAttribute` 11 | * `Model::save` 12 | * `Model::toArray` 13 | * `Model::replicate` 14 | * `Model::isDirty` 15 | * `Model::__isset` 16 | * `Model::__unset` 17 | 18 | and all methods available on the `Illuminate\Database\Eloquent\Builder` class. 19 | 20 | ## Installation 21 | 22 | Clone the repo or pull as composer dependency: 23 | 24 | ``` 25 | composer require sofa/hookable:~5.2 26 | ``` 27 | 28 | ## Usage 29 | 30 | In order to register a hook you use static method `hook` on the model: [example](https://github.com/jarektkaczyk/eloquence/blob/5.1/src/Mappable.php#L42-L56). 31 | 32 | **Important** Due to the fact that PHP will not let you bind a `Closure` to your model's instance if it is created **in a static context** (for example model's `boot` method), you need to hack it a little bit, in that the closure is created in an object context. 33 | 34 | For example see the above example along with the [class that encloses our closures in an instance scope](https://github.com/jarektkaczyk/eloquence/blob/5.1/src/Mappable/Hooks.php) that is being used there. 35 | 36 | Signature for the hook closure is following: 37 | 38 | ```php 39 | function (Closure $next, mixed $payload, Sofa\Hookable\Contracts\ArgumentBag $args) 40 | ``` 41 | 42 | Hooks are resolved via `Sofa\Hookable\Pipeline` in the same order they were registered (except for `setAttribute` where the order is reversed), and each is called unless you return early: 43 | 44 | ```php 45 | // example hook on getAttribute method: 46 | function ($next, $value, $args) 47 | { 48 | if (/* your condition */) { 49 | // return early 50 | return 'some value'; // or the $value 51 | } 52 | 53 | else if (/* other condition */) { 54 | // you may want to mutate the value 55 | $value = strtolower($value); 56 | } 57 | 58 | // finally continue calling other hooks 59 | return $next($value, $args); 60 | } 61 | ``` 62 | 63 | ## Contribution 64 | 65 | All contributions are welcome, PRs must be **tested** and **PSR-2 compliant**. 66 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sofa/hookable", 3 | "description": "Laravel Eloquent hooks system.", 4 | "license": "MIT", 5 | "support": { 6 | "issues": "https://github.com/jarektkaczyk/hookable/issues", 7 | "source": "https://github.com/jarektkaczyk/hookable" 8 | }, 9 | "keywords": ["eloquent", "laravel"], 10 | "authors": [ 11 | { 12 | "name": "Jarek Tkaczyk", 13 | "email": "jarek@softonsofa.com", 14 | "homepage": "https://softonsofa.com/", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=7.0", 20 | "illuminate/database": ">=5.4" 21 | }, 22 | "require-dev": { 23 | "kahlan/kahlan": "~1.1" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Sofa\\Hookable\\": "src/" 28 | } 29 | }, 30 | "config": { 31 | "preferred-install": "dist" 32 | }, 33 | "scripts": { 34 | "test": "vendor/bin/kahlan" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/ArgumentBag.php: -------------------------------------------------------------------------------- 1 | items = $items; 22 | } 23 | 24 | /** 25 | * @inheritdoc 26 | */ 27 | public function all() 28 | { 29 | return $this->items; 30 | } 31 | 32 | /** 33 | * @inheritdoc 34 | */ 35 | public function first() 36 | { 37 | $items = $this->items; 38 | 39 | return $this->isEmpty() ? null : reset($items); 40 | } 41 | 42 | /** 43 | * @inheritdoc 44 | */ 45 | public function last() 46 | { 47 | $items = array_reverse($this->items); 48 | 49 | return $this->isEmpty() ? null : reset($items); 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function get($key, $default = null) 56 | { 57 | return array_key_exists($key, $this->items) ? $this->items[$key] : $default; 58 | } 59 | 60 | /** 61 | * Set value at given key. 62 | * 63 | * @param string $key 64 | * @param mixed $value 65 | */ 66 | public function set($key, $value) 67 | { 68 | $this->items[$key] = $value; 69 | } 70 | 71 | /** 72 | * @inheritdoc 73 | */ 74 | public function isEmpty() 75 | { 76 | return ! count($this->items); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Builder.php: -------------------------------------------------------------------------------- 1 | ', '<=', '>=', '<>', '!=', '<=>', 21 | 'like', 'like binary', 'not like', 'between', 'ilike', 22 | '&', '|', '^', '<<', '>>', 23 | 'rlike', 'regexp', 'not regexp', 24 | '~', '~*', '!~', '!~*', 'similar to', 25 | 'not similar to', 'not ilike', '~~*', '!~~*', 26 | ]; 27 | 28 | /** 29 | * The methods that should be returned from query builder. 30 | * 31 | * @var array 32 | */ 33 | protected $passthru = [ 34 | 'toSql', 'lists', 'insert', 'insertGetId', 'pluck', 'value', 'count', 'raw', 'min', 'max', 35 | 'avg', 'sum', 'exists', 'doesntExist', 'getBindings', 'aggregate', 'getConnection' 36 | ]; 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Hooks handling 41 | |-------------------------------------------------------------------------- 42 | */ 43 | 44 | /** 45 | * Call base Eloquent method. 46 | * 47 | * @param string $method 48 | * @param array $args 49 | * @return mixed 50 | */ 51 | public function callParent($method, array $args) 52 | { 53 | return call_user_func_array("parent::{$method}", $args); 54 | } 55 | 56 | /** 57 | * Call custom handlers for where call. 58 | * 59 | * @param string $method 60 | * @param \Sofa\Hookable\ArgumentBag $args 61 | * @return mixed 62 | */ 63 | protected function callHook($method, ArgumentBag $args) 64 | { 65 | if ($this->hasHook($args->get('column')) || in_array($method, ['select', 'addSelect'])) { 66 | return $this->getModel()->queryHook($this, $method, $args); 67 | } 68 | 69 | return $this->callParent($method, $args->all()); 70 | } 71 | 72 | /** 73 | * Determine whether where call might have custom handler. 74 | * 75 | * @param string $column 76 | * @return boolean 77 | */ 78 | protected function hasHook($column) 79 | { 80 | // If developer provided column prefixed with table name we will 81 | // not even try to map the column, since obviously the value 82 | // refers to the actual column name on the queried table. 83 | return is_string($column) && strpos($column, '.') === false; 84 | } 85 | 86 | /** 87 | * Pack arguments in ArgumentBag instance. 88 | * 89 | * @param array $args 90 | * @return \Sofa\Hookable\ArgumentBag 91 | */ 92 | protected function packArgs(array $args) 93 | { 94 | return new ArgumentBag($args); 95 | } 96 | 97 | /* 98 | |-------------------------------------------------------------------------- 99 | | Query builder overrides 100 | |-------------------------------------------------------------------------- 101 | */ 102 | 103 | /** 104 | * Set the columns to be selected. 105 | * 106 | * @param array $columns 107 | * @return $this 108 | */ 109 | public function select($columns = ['*']) 110 | { 111 | $columns = is_array($columns) ? $columns : func_get_args(); 112 | 113 | return $this->callHook(__FUNCTION__, $this->packArgs(compact('columns'))); 114 | } 115 | 116 | /** 117 | * Add where constraint to the query. 118 | * 119 | * @param mixed $column 120 | * @param string $operator 121 | * @param mixed $value 122 | * @param string $boolean 123 | * @return $this 124 | */ 125 | public function where($column, $operator = null, $value = null, $boolean = 'and') 126 | { 127 | if (!in_array(strtolower($operator), $this->operators, true)) { 128 | list($value, $operator) = [$operator, '=']; 129 | } 130 | 131 | $bag = $this->packArgs(compact('column', 'operator', 'value', 'boolean')); 132 | 133 | return $this->callHook(__FUNCTION__, $bag); 134 | } 135 | 136 | /** 137 | * Add an "or where" clause to the query. 138 | * 139 | * @param string $column 140 | * @param string $operator 141 | * @param mixed $value 142 | * @return $this 143 | */ 144 | public function orWhere($column, $operator = null, $value = null) 145 | { 146 | return $this->where($column, $operator, $value, 'or'); 147 | } 148 | 149 | /** 150 | * Add a where between statement to the query. 151 | * 152 | * @param string $column 153 | * @param array $values 154 | * @param string $boolean 155 | * @param boolean $not 156 | * @return $this 157 | * 158 | * @throws \InvalidArgumentException 159 | */ 160 | public function whereBetween($column, array $values, $boolean = 'and', $not = false) 161 | { 162 | if (($count = count($values)) != 2) { 163 | throw new InvalidArgumentException( 164 | "Between clause requires exactly 2 values, {$count} given." 165 | ); 166 | } 167 | 168 | return $this->callHook(__FUNCTION__, $this->packArgs(compact('column', 'values', 'boolean', 'not'))); 169 | } 170 | 171 | /** 172 | * Add an or where between statement to the query. 173 | * 174 | * @param string $column 175 | * @param array $values 176 | * @return $this 177 | */ 178 | public function orWhereBetween($column, array $values) 179 | { 180 | return $this->whereBetween($column, $values, 'or'); 181 | } 182 | 183 | /** 184 | * Add a where not between statement to the query. 185 | * 186 | * @param string $column 187 | * @param array $values 188 | * @param string $boolean 189 | * @return $this 190 | */ 191 | public function whereNotBetween($column, array $values, $boolean = 'and') 192 | { 193 | return $this->whereBetween($column, $values, $boolean, true); 194 | } 195 | 196 | /** 197 | * Add an or where not between statement to the query. 198 | * 199 | * @param string $column 200 | * @param array $values 201 | * @return $this 202 | */ 203 | public function orWhereNotBetween($column, array $values) 204 | { 205 | return $this->whereNotBetween($column, $values, 'or'); 206 | } 207 | 208 | /** 209 | * Add a "where in" clause to the query. 210 | * 211 | * @param string $column 212 | * @param mixed $values 213 | * @param string $boolean 214 | * @param bool $not 215 | * @return $this 216 | */ 217 | public function whereIn($column, $values, $boolean = 'and', $not = false) 218 | { 219 | return $this->callHook(__FUNCTION__, $this->packArgs(compact('column', 'values', 'boolean', 'not'))); 220 | } 221 | 222 | /** 223 | * Add an "or where in" clause to the query. 224 | * 225 | * @param string $column 226 | * @param mixed $values 227 | * @return $this 228 | */ 229 | public function orWhereIn($column, $values) 230 | { 231 | return $this->whereIn($column, $values, 'or'); 232 | } 233 | 234 | /** 235 | * Add a "where not in" clause to the query. 236 | * 237 | * @param string $column 238 | * @param mixed $values 239 | * @param string $boolean 240 | * @return $this 241 | */ 242 | public function whereNotIn($column, $values, $boolean = 'and') 243 | { 244 | return $this->whereIn($column, $values, $boolean, true); 245 | } 246 | 247 | /** 248 | * Add an "or where not in" clause to the query. 249 | * 250 | * @param string $column 251 | * @param mixed $values 252 | * @return $this 253 | */ 254 | public function orWhereNotIn($column, $values) 255 | { 256 | return $this->whereNotIn($column, $values, 'or'); 257 | } 258 | 259 | /** 260 | * Add a "where null" clause to the query. 261 | * 262 | * @param string $column 263 | * @param string $boolean 264 | * @param bool $not 265 | * @return $this 266 | */ 267 | public function whereNull($column, $boolean = 'and', $not = false) 268 | { 269 | return $this->callHook(__FUNCTION__, $this->packArgs(compact('column', 'boolean', 'not'))); 270 | } 271 | 272 | /** 273 | * Add an "or where null" clause to the query. 274 | * 275 | * @param string $column 276 | * @return $this 277 | */ 278 | public function orWhereNull($column) 279 | { 280 | return $this->whereNull($column, 'or'); 281 | } 282 | 283 | /** 284 | * Add a "where not null" clause to the query. 285 | * 286 | * @param string $column 287 | * @param string $boolean 288 | * @return $this 289 | */ 290 | public function whereNotNull($column, $boolean = 'and') 291 | { 292 | return $this->whereNull($column, $boolean, true); 293 | } 294 | 295 | /** 296 | * Add an "or where not null" clause to the query. 297 | * 298 | * @param string $column 299 | * @return $this 300 | */ 301 | public function orWhereNotNull($column) 302 | { 303 | return $this->whereNotNull($column, 'or'); 304 | } 305 | 306 | /** 307 | * Add a date based (year, month, day) statement to the query. 308 | * 309 | * @param string $type 310 | * @param string $column 311 | * @param string $operator 312 | * @param int $value 313 | * @param string $boolean 314 | * @return $this 315 | */ 316 | protected function addDateBasedWhere($type, $column, $operator, $value, $boolean = 'and') 317 | { 318 | return $this->callHook("where{$type}", $this->packArgs(compact('column', 'operator', 'value', 'boolean'))); 319 | } 320 | 321 | /** 322 | * Add a "where date" statement to the query. 323 | * 324 | * @param string $column 325 | * @param string $operator 326 | * @param int $value 327 | * @param string $boolean 328 | * @return $this 329 | */ 330 | public function whereDate($column, $operator, $value, $boolean = 'and') 331 | { 332 | return $this->addDateBasedWhere('Date', $column, $operator, $value, $boolean); 333 | } 334 | 335 | /** 336 | * Add a "where day" statement to the query. 337 | * 338 | * @param string $column 339 | * @param string $operator 340 | * @param int $value 341 | * @param string $boolean 342 | * @return $this 343 | */ 344 | public function whereDay($column, $operator, $value, $boolean = 'and') 345 | { 346 | return $this->addDateBasedWhere('Day', $column, $operator, $value, $boolean); 347 | } 348 | 349 | /** 350 | * Add a "where month" statement to the query. 351 | * 352 | * @param string $column 353 | * @param string $operator 354 | * @param int $value 355 | * @param string $boolean 356 | * @return $this 357 | */ 358 | public function whereMonth($column, $operator, $value, $boolean = 'and') 359 | { 360 | return $this->addDateBasedWhere('Month', $column, $operator, $value, $boolean); 361 | } 362 | 363 | /** 364 | * Add a "where year" statement to the query. 365 | * 366 | * @param string $column 367 | * @param string $operator 368 | * @param int $value 369 | * @param string $boolean 370 | * @return $this 371 | */ 372 | public function whereYear($column, $operator, $value, $boolean = 'and') 373 | { 374 | return $this->addDateBasedWhere('Year', $column, $operator, $value, $boolean); 375 | } 376 | 377 | /** 378 | * Add an exists clause to the query. 379 | * 380 | * @param \Closure $callback 381 | * @param string $boolean 382 | * @param bool $not 383 | * @return $this 384 | */ 385 | public function whereExists(Closure $callback, $boolean = 'and', $not = false) 386 | { 387 | $type = $not ? 'NotExists' : 'Exists'; 388 | 389 | $builder = $this->newQuery(); 390 | 391 | call_user_func($callback, $builder); 392 | 393 | $query = $builder->getQuery(); 394 | 395 | $this->query->wheres[] = compact('type', 'query', 'boolean'); 396 | 397 | $this->query->mergeBindings($query); 398 | 399 | return $this; 400 | } 401 | 402 | /** 403 | * Add an "order by" clause to the query. 404 | * 405 | * @param string $column 406 | * @param string $direction 407 | * @return $this 408 | */ 409 | public function orderBy($column, $direction = 'asc') 410 | { 411 | return $this->callHook(__FUNCTION__, $this->packArgs(compact('column', 'direction'))); 412 | } 413 | 414 | /** 415 | * Add an "order by" clause for a timestamp to the query. 416 | * 417 | * @param string $column 418 | * @return $this 419 | */ 420 | public function latest($column = 'created_at') 421 | { 422 | return $this->orderBy($column, 'desc'); 423 | } 424 | 425 | /** 426 | * Add an "order by" clause for a timestamp to the query. 427 | * 428 | * @param string $column 429 | * @return $this 430 | */ 431 | public function oldest($column = 'created_at') 432 | { 433 | return $this->orderBy($column, 'asc'); 434 | } 435 | 436 | /** 437 | * Get an array with the values of a given column. 438 | * 439 | * @param string $column 440 | * @param string|null $key 441 | * @return array 442 | */ 443 | public function pluck($column, $key = null) 444 | { 445 | return $this->callHook(__FUNCTION__, $this->packArgs(compact('column', 'key'))); 446 | } 447 | 448 | /** 449 | * Get a single column's value from the first result of a query. 450 | * 451 | * @param string $column 452 | * @return mixed 453 | */ 454 | public function value($column) 455 | { 456 | return $this->callHook(__FUNCTION__, $this->packArgs(compact('column'))); 457 | } 458 | 459 | /** 460 | * Execute an aggregate function on the database. 461 | * 462 | * @param string $function 463 | * @param array $columns 464 | * @return mixed 465 | */ 466 | public function aggregate($function, array $columns = ['*']) 467 | { 468 | $column = (reset($columns) !== '*') ? reset($columns) : null; 469 | 470 | return $this->callHook(__FUNCTION__, $this->packArgs(compact('function', 'columns', 'column'))); 471 | } 472 | 473 | /** 474 | * Retrieve the minimum value of a given column. 475 | * 476 | * @param string $column 477 | * @return mixed 478 | */ 479 | public function min($column) 480 | { 481 | return $this->aggregate(__FUNCTION__, (array) $column); 482 | } 483 | 484 | /** 485 | * Retrieve the maximum value of a given column. 486 | * 487 | * @param string $column 488 | * @return mixed 489 | */ 490 | public function max($column) 491 | { 492 | return $this->aggregate(__FUNCTION__, (array) $column); 493 | } 494 | 495 | /** 496 | * Retrieve the average of the values of a given column. 497 | * 498 | * @param string $column 499 | * @return mixed 500 | */ 501 | public function avg($column) 502 | { 503 | return $this->aggregate(__FUNCTION__, (array) $column); 504 | } 505 | 506 | /** 507 | * Retrieve the sum of the values of a given column. 508 | * 509 | * @param string $column 510 | * @return mixed 511 | */ 512 | public function sum($column) 513 | { 514 | return $this->aggregate(__FUNCTION__, (array) $column); 515 | } 516 | 517 | /** 518 | * Retrieve the "count" result of the query. 519 | * 520 | * @param string $columns 521 | * @return int 522 | */ 523 | public function count($columns = '*') 524 | { 525 | return $this->aggregate(__FUNCTION__, (array) $columns); 526 | } 527 | 528 | /** 529 | * Get an array with the values of a given column. 530 | * 531 | * @param string $column 532 | * @param string $key 533 | * @return array 534 | */ 535 | public function lists($column, $key = null) 536 | { 537 | return $this->pluck($column, $key); 538 | } 539 | 540 | /** 541 | * Get a new instance of the Hookable query builder. 542 | * 543 | * @return \Sofa\Hookable\Builder 544 | */ 545 | public function newQuery() 546 | { 547 | return $this->model->newQueryWithoutScopes(); 548 | } 549 | } 550 | -------------------------------------------------------------------------------- /src/Contracts/ArgumentBag.php: -------------------------------------------------------------------------------- 1 | boundHooks(__FUNCTION__); 70 | $params = compact('method', 'args'); 71 | $payload = $query; 72 | $destination = function ($query) use ($method, $args) { 73 | return call_user_func_array([$query, 'callParent'], [$method, $args->all()]); 74 | }; 75 | 76 | return $this->pipe($hooks, $payload, $params, $destination); 77 | } 78 | 79 | /** 80 | * Register hook for getAttribute. 81 | * 82 | * @param string $key 83 | * @return mixed 84 | * @return mixed 85 | */ 86 | public function getAttribute($key) 87 | { 88 | $hooks = $this->boundHooks(__FUNCTION__); 89 | $params = compact('key'); 90 | $payload = parent::getAttribute($key); 91 | $destination = function ($attribute) { 92 | return $attribute; 93 | }; 94 | 95 | return $this->pipe($hooks, $payload, $params, $destination); 96 | } 97 | 98 | /** 99 | * Register hook for setAttribute. 100 | * 101 | * @param string $key 102 | * @param mixed $value 103 | * @return mixed 104 | */ 105 | public function setAttribute($key, $value) 106 | { 107 | $hooks = array_reverse($this->boundHooks(__FUNCTION__)); 108 | $params = compact('key'); 109 | $payload = $value; 110 | $destination = function ($value) use ($key) { 111 | parent::setAttribute($key, $value); 112 | }; 113 | 114 | $this->pipe($hooks, $payload, $params, $destination); 115 | 116 | return $this; 117 | } 118 | 119 | /** 120 | * Register hook for save. 121 | * 122 | * @param array $options 123 | * @return boolean 124 | */ 125 | public function save(array $options = []) 126 | { 127 | if (!($saved = parent::save($options)) && $this->isDirty()) { 128 | return false; 129 | } 130 | 131 | $hooks = $this->boundHooks(__FUNCTION__); 132 | $params = compact('options'); 133 | $payload = true; 134 | $destination = function () use ($saved) { 135 | return $saved; 136 | }; 137 | 138 | return $this->pipe($hooks, $payload, $params, $destination); 139 | } 140 | 141 | /** 142 | * Register hook for isDirty. 143 | * 144 | * @param null $attributes 145 | * @return bool 146 | */ 147 | public function isDirty($attributes = null) 148 | { 149 | if (! is_array($attributes)) { 150 | $attributes = func_get_args(); 151 | } 152 | 153 | $hooks = $this->boundHooks(__FUNCTION__); 154 | $params = compact('attributes'); 155 | $payload = $attributes; 156 | $destination = function ($attributes) { 157 | return parent::isDirty($attributes); 158 | }; 159 | 160 | return $this->pipe($hooks, $payload, $params, $destination); 161 | } 162 | 163 | /** 164 | * Register hook for toArray. 165 | * 166 | * @return mixed 167 | */ 168 | public function toArray() 169 | { 170 | $hooks = $this->boundHooks(__FUNCTION__); 171 | $params = []; 172 | $payload = parent::toArray(); 173 | $destination = function ($array) { 174 | return $array; 175 | }; 176 | 177 | return $this->pipe($hooks, $payload, $params, $destination); 178 | } 179 | 180 | /** 181 | * Register hook for replicate. 182 | * 183 | * @param array|null $except 184 | * 185 | * @return mixed 186 | */ 187 | public function replicate(array $except = null) 188 | { 189 | $hooks = $this->boundHooks(__FUNCTION__); 190 | $params = ['except' => $except, 'original' => $this]; 191 | $payload = parent::replicate($except); 192 | $destination = function ($copy) { 193 | return $copy; 194 | }; 195 | 196 | return $this->pipe($hooks, $payload, $params, $destination); 197 | } 198 | 199 | /** 200 | * Register hook for isset call. 201 | * 202 | * @param string $key 203 | * @return boolean 204 | */ 205 | public function __isset($key) 206 | { 207 | $hooks = $this->boundHooks(__FUNCTION__); 208 | $params = compact('key'); 209 | $payload = parent::__isset($key); 210 | $destination = function ($isset) { 211 | return $isset; 212 | }; 213 | 214 | return $this->pipe($hooks, $payload, $params, $destination); 215 | } 216 | 217 | /** 218 | * Register hook for isset call. 219 | * 220 | * @param string $key 221 | * @return boolean 222 | */ 223 | public function __unset($key) 224 | { 225 | $hooks = $this->boundHooks(__FUNCTION__); 226 | $params = compact('key'); 227 | $payload = false; 228 | $destination = function () use ($key) { 229 | return call_user_func('parent::__unset', $key); 230 | }; 231 | 232 | return $this->pipe($hooks, $payload, $params, $destination); 233 | } 234 | 235 | /** 236 | * Send payload through the pipeline. 237 | * 238 | * @param \Closure[] $pipes 239 | * @param mixed $payload 240 | * @param array $params 241 | * @param \Closure $destination 242 | * @return mixed 243 | */ 244 | protected function pipe($pipes, $payload, $params, $destination) 245 | { 246 | return (new Pipeline($pipes)) 247 | ->send($payload) 248 | ->with(new ArgumentBag($params)) 249 | ->to($destination); 250 | } 251 | 252 | /** 253 | * Get all hooks for given method bound to $this instance. 254 | * 255 | * @param string $method 256 | * @return \Closure[] 257 | */ 258 | protected function boundHooks($method) 259 | { 260 | $hooks = isset(static::$hooks[$method]) ? static::$hooks[$method] : []; 261 | 262 | return array_map(function ($hook) { 263 | return $hook->bindTo($this, get_class($this)); 264 | }, $hooks); 265 | } 266 | } 267 | -------------------------------------------------------------------------------- /src/Pipeline.php: -------------------------------------------------------------------------------- 1 | pipes = $pipes; 40 | } 41 | 42 | /** 43 | * @inheritdoc 44 | */ 45 | public function send($parcel) 46 | { 47 | $this->parcel = $parcel; 48 | 49 | return $this; 50 | } 51 | 52 | /** 53 | * @inheritdoc 54 | */ 55 | public function through(array $pipes) 56 | { 57 | $this->pipes = $pipes; 58 | 59 | return $this; 60 | } 61 | 62 | /** 63 | * @inheritdoc 64 | */ 65 | public function with(ArgumentBagContract $args) 66 | { 67 | $this->args = $args; 68 | 69 | return $this; 70 | } 71 | 72 | /** 73 | * @inheritdoc 74 | */ 75 | public function to(Closure $destination) 76 | { 77 | $initialStack = $destination; 78 | 79 | // Actions are stacked from end to beginning, so let's reverse them. 80 | $pipes = array_reverse($this->pipes); 81 | 82 | $route = array_reduce($pipes, function ($stack, $pipe) { 83 | return function ($parcel, $args = null) use ($stack, $pipe) { 84 | return $pipe($stack, $parcel, $args); 85 | }; 86 | }, $initialStack); 87 | 88 | return call_user_func_array($route, [$this->parcel, $this->args]); 89 | } 90 | } 91 | --------------------------------------------------------------------------------