From 7a4b6dfb4df1cb513d802432ae3ace0841bf8777 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 8 Dec 2021 12:46:06 -0500 Subject: [PATCH 01/19] Initial static implementation --- src/Illuminate/Support/Hookable.php | 13 +++ src/Illuminate/Support/Hooks/Hook.php | 20 +++++ .../Support/Hooks/HookCollection.php | 80 +++++++++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 src/Illuminate/Support/Hookable.php create mode 100644 src/Illuminate/Support/Hooks/Hook.php create mode 100644 src/Illuminate/Support/Hooks/HookCollection.php diff --git a/src/Illuminate/Support/Hookable.php b/src/Illuminate/Support/Hookable.php new file mode 100644 index 000000000000..9dae3319b93c --- /dev/null +++ b/src/Illuminate/Support/Hookable.php @@ -0,0 +1,13 @@ +run($name, $arguments); + } +} diff --git a/src/Illuminate/Support/Hooks/Hook.php b/src/Illuminate/Support/Hooks/Hook.php new file mode 100644 index 000000000000..8a1ec31b9066 --- /dev/null +++ b/src/Illuminate/Support/Hooks/Hook.php @@ -0,0 +1,20 @@ +callback, $arguments); + } +} diff --git a/src/Illuminate/Support/Hooks/HookCollection.php b/src/Illuminate/Support/Hooks/HookCollection.php new file mode 100644 index 000000000000..d014ea34a8ac --- /dev/null +++ b/src/Illuminate/Support/Hooks/HookCollection.php @@ -0,0 +1,80 @@ +push(new Hook($prefix, Closure::fromCallable([$class, $method]))); + } + } + }); + } + + protected static function loadHooks($class) + { + $classNames = array_merge([$class => $class], class_parents($class), class_implements($class)); + + return collect((new ReflectionClass($class))->getMethods()) + ->map(fn($method) => static::hookForMethod($method, $classNames)) + ->filter(); + } + + protected static function hookForMethod(ReflectionMethod $method, array $classNames): ?Hook + { + if (static::methodReturnsHook($method)) { + return $method->invoke(null); + } + + return null; + } + + protected static function methodReturnsHook(ReflectionMethod $method): bool + { + return $method->isStatic() + && $method->getReturnType() instanceof ReflectionNamedType + && $method->getReturnType()->getName() === Hook::class; + } + + public function run($name, ...$arguments) + { + $this->where('name', $name) + ->sortBy('weight') + ->each(fn($hook) => $hook->run($arguments)); + } +} From 08358507ca338de072f623f839096ed66dba409a Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 8 Dec 2021 13:37:49 -0500 Subject: [PATCH 02/19] Refactor --- src/Illuminate/Support/Hookable.php | 9 ++- src/Illuminate/Support/Hooks/Hook.php | 28 +++++++-- .../Support/Hooks/HookCollection.php | 60 +++++++++++-------- src/Illuminate/Support/Hooks/PendingHook.php | 36 +++++++++++ 4 files changed, 101 insertions(+), 32 deletions(-) create mode 100644 src/Illuminate/Support/Hooks/PendingHook.php diff --git a/src/Illuminate/Support/Hookable.php b/src/Illuminate/Support/Hookable.php index 9dae3319b93c..e8de817194b2 100644 --- a/src/Illuminate/Support/Hookable.php +++ b/src/Illuminate/Support/Hookable.php @@ -6,8 +6,13 @@ trait Hookable { - protected static function runHooks($name, ...$arguments) + protected static function runStaticHooks($name, ...$arguments) { - HookCollection::for(static::class)->run($name, $arguments); + HookCollection::for(static::class)->run($name, null, $arguments); + } + + protected function runHooks($name, ...$arguments) + { + HookCollection::for(static::class)->run($name, $this, $arguments); } } diff --git a/src/Illuminate/Support/Hooks/Hook.php b/src/Illuminate/Support/Hooks/Hook.php index 8a1ec31b9066..51661f4fdd1b 100644 --- a/src/Illuminate/Support/Hooks/Hook.php +++ b/src/Illuminate/Support/Hooks/Hook.php @@ -6,15 +6,35 @@ class Hook { + public const PRIORITY_HIGH = 100; + public const PRIORITY_NORMAL = 200; + public const PRIORITY_LOW = 300; + + public static function highPriority(string $name, Closure $callback): Hook + { + return new static($name, $callback, self::PRIORITY_HIGH); + } + + public static function make(string $name, Closure $callback): Hook + { + return new static($name, $callback, self::PRIORITY_NORMAL); + } + + public static function lowPriority(string $name, Closure $callback): Hook + { + return new static($name, $callback, self::PRIORITY_LOW); + } + public function __construct( public string $name, public Closure $callback, - public bool $isStatic = true, - public int $weight = 100 + public int $priority = self::PRIORITY_NORMAL ) { } - public function run(array $arguments) + public function run($instance = null, array $arguments = []) { - return call_user_func_array($this->callback, $arguments); + return $instance + ? $this->callback->call($instance, ...$arguments) + : call_user_func_array($this->callback, $arguments); } } diff --git a/src/Illuminate/Support/Hooks/HookCollection.php b/src/Illuminate/Support/Hooks/HookCollection.php index d014ea34a8ac..0e7ba4061136 100644 --- a/src/Illuminate/Support/Hooks/HookCollection.php +++ b/src/Illuminate/Support/Hooks/HookCollection.php @@ -28,53 +28,61 @@ public static function clearCache() static::$cache = []; } - public static function register($className, Closure $callback) - { - static::$registrars[$className][] = $callback; - } - - public static function registerTraitPrefix($className, $prefix) - { - static::register($className, function($hooks, $class) use ($prefix) { - foreach (class_uses_recursive($class) as $trait) { - $method = $prefix.class_basename($trait); - - if (method_exists($class, $method)) { - $hooks->push(new Hook($prefix, Closure::fromCallable([$class, $method]))); - } - } - }); - } + //public static function register($className, Closure $callback) + //{ + // static::$registrars[$className][] = $callback; + //} + // + //public static function registerTraitPrefix($className, $prefix) + //{ + // static::register($className, function($hooks, $class) use ($prefix) { + // foreach (class_uses_recursive($class) as $trait) { + // $method = $prefix.class_basename($trait); + // + // if (method_exists($class, $method)) { + // $hooks->push(new Hook($prefix, Closure::fromCallable([$class, $method]))); + // } + // } + // }); + //} protected static function loadHooks($class) { - $classNames = array_merge([$class => $class], class_parents($class), class_implements($class)); + $classNames = array_values(array_merge( + [$class], class_parents($class), class_implements($class) + )); return collect((new ReflectionClass($class))->getMethods()) ->map(fn($method) => static::hookForMethod($method, $classNames)) ->filter(); } - protected static function hookForMethod(ReflectionMethod $method, array $classNames): ?Hook + protected static function hookForMethod(ReflectionMethod $method, array $classNames): ?PendingHook { if (static::methodReturnsHook($method)) { - return $method->invoke(null); + return new PendingHook(static function($instance = null) use ($method) { + return $method->invoke($instance); + }, $method->isStatic()); } + // FIXME: Allow for registered hooks by name + return null; } protected static function methodReturnsHook(ReflectionMethod $method): bool { - return $method->isStatic() - && $method->getReturnType() instanceof ReflectionNamedType + return $method->getReturnType() instanceof ReflectionNamedType && $method->getReturnType()->getName() === Hook::class; } - public function run($name, ...$arguments) + public function run($name, $instance = null, $arguments = []) { - $this->where('name', $name) - ->sortBy('weight') - ->each(fn($hook) => $hook->run($arguments)); + $hooks = $this->where('isStatic', is_null($instance)) + ->map(fn(PendingHook $pending) => $pending->resolve($instance)); + + $hooks->where('name', $name) + ->sortBy('priority') + ->each(fn(Hook $hook) => $hook->run($instance, $arguments)); } } diff --git a/src/Illuminate/Support/Hooks/PendingHook.php b/src/Illuminate/Support/Hooks/PendingHook.php new file mode 100644 index 000000000000..3e82ded6999a --- /dev/null +++ b/src/Illuminate/Support/Hooks/PendingHook.php @@ -0,0 +1,36 @@ +isStatic && is_null($instance)) { + throw new RuntimeException('Trying to resolve a non-static hook statically.'); + } + + return $this->getHook($instance); + } + + protected function getHook($instance = null): Hook + { + if (is_null($this->hook)) { + $this->hook = $this->isStatic + ? call_user_func($this->callback) + : $this->callback->call($instance); + } + + return $this->hook; + } +} From 4f1b50f8b9088bf2f5f4c4bfa31ff7c2ad1256d9 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 8 Dec 2021 15:23:45 -0500 Subject: [PATCH 03/19] Refactor to interface --- src/Illuminate/Contracts/Support/Hook.php | 43 ++++++ .../Console/Seeds/WithoutModelEvents.php | 19 ++- src/Illuminate/Database/Seeder.php | 19 ++- src/Illuminate/Support/Hookable.php | 50 ++++++- src/Illuminate/Support/Hooks/Hook.php | 91 ++++++++++-- .../Support/Hooks/HookCollection.php | 129 +++++++++++++----- src/Illuminate/Support/Hooks/PendingHook.php | 30 +++- src/Illuminate/Support/Hooks/TraitHook.php | 46 +++++++ 8 files changed, 357 insertions(+), 70 deletions(-) create mode 100644 src/Illuminate/Contracts/Support/Hook.php create mode 100644 src/Illuminate/Support/Hooks/TraitHook.php diff --git a/src/Illuminate/Contracts/Support/Hook.php b/src/Illuminate/Contracts/Support/Hook.php new file mode 100644 index 000000000000..4e18c7b5dfe8 --- /dev/null +++ b/src/Illuminate/Contracts/Support/Hook.php @@ -0,0 +1,43 @@ + Model::withoutEvents($callback); + return Hook::make('invoke', static function () { + if (! $dispatcher = Model::getEventDispatcher()) { + return null; + } + + Model::setEventDispatcher(new NullDispatcher($dispatcher)); + + return static fn() => Model::setEventDispatcher($dispatcher); + }); } } diff --git a/src/Illuminate/Database/Seeder.php b/src/Illuminate/Database/Seeder.php index 1a7a12e1914d..615f7d0d9730 100755 --- a/src/Illuminate/Database/Seeder.php +++ b/src/Illuminate/Database/Seeder.php @@ -6,10 +6,13 @@ use Illuminate\Container\Container; use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Support\Arr; +use Illuminate\Support\Hookable; use InvalidArgumentException; abstract class Seeder { + use Hookable; + /** * The container instance. * @@ -171,16 +174,10 @@ public function __invoke(array $parameters = []) throw new InvalidArgumentException('Method [run] missing from '.get_class($this)); } - $callback = fn () => isset($this->container) - ? $this->container->call([$this, 'run'], $parameters) - : $this->run(...$parameters); - - $uses = array_flip(class_uses_recursive(static::class)); - - if (isset($uses[WithoutModelEvents::class])) { - $callback = $this->withoutModelEvents($callback); - } - - return $callback(); + return $this->runHooks('invoke', $this, function() use ($parameters) { + return isset($this->container) + ? $this->container->call([$this, 'run'], $parameters) + : $this->run(...$parameters); + }); } } diff --git a/src/Illuminate/Support/Hookable.php b/src/Illuminate/Support/Hookable.php index e8de817194b2..f776dea76d8c 100644 --- a/src/Illuminate/Support/Hookable.php +++ b/src/Illuminate/Support/Hookable.php @@ -2,17 +2,59 @@ namespace Illuminate\Support; +use Closure; use Illuminate\Support\Hooks\HookCollection; +use Illuminate\Support\Hooks\TraitHook; trait Hookable { - protected static function runStaticHooks($name, ...$arguments) + /** + * Run hooks statically. + * + * @param string $name + * @param array $arguments + * @param \Closure|null $callback + * @return mixed + */ + protected static function runStaticHooks($name, $arguments = [], Closure $callback = null) { - HookCollection::for(static::class)->run($name, null, $arguments); + return HookCollection::for(static::class)->run($name, static::class, $arguments, $callback); } - protected function runHooks($name, ...$arguments) + /** + * Run trait hooks statically. + * + * @param string $prefix + * @param array $arguments + * @return void + */ + protected static function runStaticTraitHooks($prefix, $arguments = []) { - HookCollection::for(static::class)->run($name, $this, $arguments); + (new TraitHook($prefix))->run(static::class, $arguments); + } + + /** + * Run hooks non-statically. + * + * @param string $name + * @param array $arguments + * @param \Closure|null $callback + * @return mixed + */ + protected function runHooks($name, $arguments = [], Closure $callback = null) + { + return HookCollection::for(static::class)->run($name, $this, $arguments, $callback); + } + + /** + * Run trait hooks. + * + * @param string $prefix + * @param array $arguments + * @return void + */ + protected function runTraitHooks($prefix, $arguments = []) + { + (new TraitHook($prefix))->run($this, $arguments); } } diff --git a/src/Illuminate/Support/Hooks/Hook.php b/src/Illuminate/Support/Hooks/Hook.php index 51661f4fdd1b..249d22a08433 100644 --- a/src/Illuminate/Support/Hooks/Hook.php +++ b/src/Illuminate/Support/Hooks/Hook.php @@ -3,38 +3,105 @@ namespace Illuminate\Support\Hooks; use Closure; +use Illuminate\Contracts\Support\Hook as HookContract; -class Hook +class Hook implements HookContract { - public const PRIORITY_HIGH = 100; - public const PRIORITY_NORMAL = 200; - public const PRIORITY_LOW = 300; + /** + * A cleanup function that should be run after the hooks are run. + * + * @var \Closure|null + */ + protected ?Closure $cleanup = null; + /** + * Instantiate a new high-priority hook. + * + * @param string $name + * @param \Closure $callback + * @return \Illuminate\Support\Hooks\Hook + */ public static function highPriority(string $name, Closure $callback): Hook { - return new static($name, $callback, self::PRIORITY_HIGH); + return new static($name, $callback, HookContract::PRIORITY_HIGH); } + /** + * Instantiate a new hook. + * + * @param string $name + * @param \Closure $callback + * @return \Illuminate\Support\Hooks\Hook + */ public static function make(string $name, Closure $callback): Hook { - return new static($name, $callback, self::PRIORITY_NORMAL); + return new static($name, $callback, HookContract::PRIORITY_NORMAL); } + /** + * Instantiate a new low-priority hook. + * + * @param string $name + * @param \Closure $callback + * @return \Illuminate\Support\Hooks\Hook + */ public static function lowPriority(string $name, Closure $callback): Hook { - return new static($name, $callback, self::PRIORITY_LOW); + return new static($name, $callback, HookContract::PRIORITY_LOW); } + /** + * Constructor + * + * @param string $name + * @param \Closure $callback + * @param int $priority + * @return void + */ public function __construct( public string $name, public Closure $callback, - public int $priority = self::PRIORITY_NORMAL + public int $priority = HookContract::PRIORITY_NORMAL ) { } - public function run($instance = null, array $arguments = []) + /** + * @inheritdoc + */ + public function run($instance, array $arguments = []) { - return $instance - ? $this->callback->call($instance, ...$arguments) - : call_user_func_array($this->callback, $arguments); + $this->cleanup = null; + + $result = $this->runCallback($this->callback, $instance, $arguments); + + if ($result instanceof Closure) { + $this->cleanup = $result; + } + } + + /** + * @inheritdoc + */ + public function cleanup($instance, array $arguments = []) + { + if (is_null($this->cleanup)) { + return; + } + + $this->runCallback($this->cleanup, $instance, $arguments); + } + + /** + * Run a callback that may or may not be static. + * + * @param Closure $callback + * @param object|string $instance + * @param array $arguments + * @return mixed + */ + protected function runCallback($callback, $instance, $arguments = []) + { + return is_object($instance) + ? $callback->call($instance, $arguments) + : $callback(...$arguments); } } diff --git a/src/Illuminate/Support/Hooks/HookCollection.php b/src/Illuminate/Support/Hooks/HookCollection.php index 0e7ba4061136..a4dfdc0b825b 100644 --- a/src/Illuminate/Support/Hooks/HookCollection.php +++ b/src/Illuminate/Support/Hooks/HookCollection.php @@ -3,6 +3,7 @@ namespace Illuminate\Support\Hooks; use Closure; +use Illuminate\Support\Arr; use Illuminate\Support\Collection; use ReflectionClass; use ReflectionMethod; @@ -10,10 +11,19 @@ class HookCollection extends Collection { + /** + * Cache of hooks that have already been loaded. + * + * @var static[] + */ protected static array $cache = []; - protected static array $registrars = []; - + /** + * Get a collection of hooks for a class or an object. + * + * @param object|string $class + * @return static + */ public static function for($class) { if (is_object($class)) { @@ -23,41 +33,38 @@ public static function for($class) return static::$cache[$class] ??= new static(static::loadHooks($class)); } + /** + * Clear the hook cache. + * + * @return void + */ public static function clearCache() { static::$cache = []; } - //public static function register($className, Closure $callback) - //{ - // static::$registrars[$className][] = $callback; - //} - // - //public static function registerTraitPrefix($className, $prefix) - //{ - // static::register($className, function($hooks, $class) use ($prefix) { - // foreach (class_uses_recursive($class) as $trait) { - // $method = $prefix.class_basename($trait); - // - // if (method_exists($class, $method)) { - // $hooks->push(new Hook($prefix, Closure::fromCallable([$class, $method]))); - // } - // } - // }); - //} - + /** + * Load the hooks for a class. + * + * @param string $class + * @return \Illuminate\Support\Collection + * + * @throws \ReflectionException + */ protected static function loadHooks($class) { - $classNames = array_values(array_merge( - [$class], class_parents($class), class_implements($class) - )); - return collect((new ReflectionClass($class))->getMethods()) - ->map(fn($method) => static::hookForMethod($method, $classNames)) + ->map(fn($method) => static::hookForMethod($method)) ->filter(); } - protected static function hookForMethod(ReflectionMethod $method, array $classNames): ?PendingHook + /** + * Load a hook for a given method based on its return type. + * + * @param \ReflectionMethod $method + * @return \Illuminate\Support\Hooks\PendingHook|null + */ + protected static function hookForMethod(ReflectionMethod $method): ?PendingHook { if (static::methodReturnsHook($method)) { return new PendingHook(static function($instance = null) use ($method) { @@ -65,24 +72,76 @@ protected static function hookForMethod(ReflectionMethod $method, array $classNa }, $method->isStatic()); } - // FIXME: Allow for registered hooks by name - return null; } + /** + * Determine if a class method returns a Hook instance. + * + * @param \ReflectionMethod $method + * @return bool + */ protected static function methodReturnsHook(ReflectionMethod $method): bool { return $method->getReturnType() instanceof ReflectionNamedType - && $method->getReturnType()->getName() === Hook::class; + && is_a($method->getReturnType()->getName(), Hook::class, true); + } + + /** + * Run all the hooks that match the given name and instance. + * + * @param string $name + * @param object|string $instance + * @param array $arguments + * @param \Closure|null $callback + * @return mixed + */ + public function run($name, $instance, $arguments = [], Closure $callback = null) + { + $arguments = Arr::wrap($arguments); + + $hooks = $this->onlyStatic(! is_object($instance)) + ->resolve($instance) + ->where('name', $name) + ->sortBy('priority'); + + $hooks->each(fn(Hook $hook) => $hook->run($instance, $arguments)); + + try { + return $callback ? $callback() : null; + } + finally { + $hooks->reverse()->each(fn(Hook $hook) => $hook->cleanup($instance, $arguments)); + } } - public function run($name, $instance = null, $arguments = []) + /** + * Filter collection to only static hooks. + * + * @param bool $onlyStatic + * @return $this + */ + public function onlyStatic($onlyStatic = true): self { - $hooks = $this->where('isStatic', is_null($instance)) - ->map(fn(PendingHook $pending) => $pending->resolve($instance)); + if ($onlyStatic) { + return $this->where('isStatic'); + } + + return $this; + } - $hooks->where('name', $name) - ->sortBy('priority') - ->each(fn(Hook $hook) => $hook->run($instance, $arguments)); + /** + * Map collection to resolved Hook instances. + * + * @param object|string $instance + * @return $this + */ + public function resolve($instance): self + { + return $this->map(function ($hook) use ($instance) { + return $hook instanceof PendingHook + ? $hook->resolve($instance) + : $hook; + }); } } diff --git a/src/Illuminate/Support/Hooks/PendingHook.php b/src/Illuminate/Support/Hooks/PendingHook.php index 3e82ded6999a..8c4c0748b257 100644 --- a/src/Illuminate/Support/Hooks/PendingHook.php +++ b/src/Illuminate/Support/Hooks/PendingHook.php @@ -7,22 +7,46 @@ class PendingHook { + /** + * The resolved hook. + * + * @var Hook|null + */ protected ?Hook $hook = null; + /** + * Constructor. + * + * @param \Closure $callback + * @param bool $isStatic + * @return void + */ public function __construct( public Closure $callback, public bool $isStatic ) { } + /** + * Resolve the pending hook into a Hook instance. + * + * @param object|string|null $instance + * @return \Illuminate\Support\Hooks\Hook + */ public function resolve($instance = null) { - if (! $this->isStatic && is_null($instance)) { - throw new RuntimeException('Trying to resolve a non-static hook statically.'); + if (is_object($instance) || $this->isStatic) { + return $this->getHook($instance); } - return $this->getHook($instance); + throw new RuntimeException('Trying to resolve a non-static hook statically.'); } + /** + * Resolve or return the already resolved Hook. + * + * @param object|string|null $instance + * @return \Illuminate\Support\Hooks\Hook + */ protected function getHook($instance = null): Hook { if (is_null($this->hook)) { diff --git a/src/Illuminate/Support/Hooks/TraitHook.php b/src/Illuminate/Support/Hooks/TraitHook.php new file mode 100644 index 000000000000..ee55d00601f6 --- /dev/null +++ b/src/Illuminate/Support/Hooks/TraitHook.php @@ -0,0 +1,46 @@ +prefix.class_basename($trait); + + if (method_exists($instance, $method)) { + if (is_object($instance)) { + $instance->$method(...$arguments); + } else { + forward_static_call_array([$instance, $method], $arguments); + } + } + } + } + + /** + * @inheritdoc + */ + public function cleanup($instance, array $arguments = []) + { + // No cleanup is necessary + } +} From 82ea0937377b1a2920e4b413846d88e40ce95645 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 8 Dec 2021 15:28:01 -0500 Subject: [PATCH 04/19] Identify other hook locations --- src/Illuminate/Console/GeneratorCommand.php | 2 ++ src/Illuminate/Database/Eloquent/Model.php | 1 + src/Illuminate/Foundation/Testing/TestCase.php | 1 + src/Illuminate/Support/Hooks/TraitHook.php | 8 +++++--- 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Console/GeneratorCommand.php b/src/Illuminate/Console/GeneratorCommand.php index 5c12e05ed094..7d7b9609fe5c 100644 --- a/src/Illuminate/Console/GeneratorCommand.php +++ b/src/Illuminate/Console/GeneratorCommand.php @@ -109,6 +109,7 @@ public function __construct(Filesystem $files) { parent::__construct(); + // TODO: Switch to Hookable if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) { $this->addTestOptions(); } @@ -165,6 +166,7 @@ public function handle() $this->info($this->type.' created successfully.'); + // TODO: Switch to Hookable if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) { $this->handleTestCreation($path); } diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 48d1d3f1b2fa..e0a36f465447 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -265,6 +265,7 @@ protected static function bootTraits() static::$traitInitializers[$class] = []; + // TODO: Switch to Hookable foreach (class_uses_recursive($class) as $trait) { $method = 'boot'.class_basename($trait); diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index 35af3d49bc0e..24b7cd77aca2 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -116,6 +116,7 @@ protected function refreshApplication() */ protected function setUpTraits() { + // TODO: Switch to Hookable $uses = array_flip(class_uses_recursive(static::class)); if (isset($uses[RefreshDatabase::class])) { diff --git a/src/Illuminate/Support/Hooks/TraitHook.php b/src/Illuminate/Support/Hooks/TraitHook.php index ee55d00601f6..26b8b7d698da 100644 --- a/src/Illuminate/Support/Hooks/TraitHook.php +++ b/src/Illuminate/Support/Hooks/TraitHook.php @@ -2,7 +2,6 @@ namespace Illuminate\Support\Hooks; -use Closure; use Illuminate\Contracts\Support\Hook as HookContract; class TraitHook implements HookContract @@ -11,10 +10,13 @@ class TraitHook implements HookContract * Constructor * * @param string $prefix + * @param int $priority * @return void */ - public function __construct(public string $prefix) - { + public function __construct( + public string $prefix, + public int $priority = HookContract::PRIORITY_NORMAL + ) { // } From 0c360b848a7ebb87c6d27ff084a9ee9e8a8a892b Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 8 Dec 2021 16:11:48 -0500 Subject: [PATCH 05/19] StyleCI --- .../Database/Console/Seeds/WithoutModelEvents.php | 2 +- src/Illuminate/Database/Seeder.php | 3 +-- src/Illuminate/Support/Hooks/Hook.php | 5 +++-- src/Illuminate/Support/Hooks/HookCollection.php | 11 +++++------ src/Illuminate/Support/Hooks/PendingHook.php | 3 ++- src/Illuminate/Support/Hooks/TraitHook.php | 2 +- 6 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Illuminate/Database/Console/Seeds/WithoutModelEvents.php b/src/Illuminate/Database/Console/Seeds/WithoutModelEvents.php index 81fba5c9904f..1fcb922c4a63 100644 --- a/src/Illuminate/Database/Console/Seeds/WithoutModelEvents.php +++ b/src/Illuminate/Database/Console/Seeds/WithoutModelEvents.php @@ -22,7 +22,7 @@ public static function withoutModelEvents(): Hook Model::setEventDispatcher(new NullDispatcher($dispatcher)); - return static fn() => Model::setEventDispatcher($dispatcher); + return static fn () => Model::setEventDispatcher($dispatcher); }); } } diff --git a/src/Illuminate/Database/Seeder.php b/src/Illuminate/Database/Seeder.php index 615f7d0d9730..be750166cf52 100755 --- a/src/Illuminate/Database/Seeder.php +++ b/src/Illuminate/Database/Seeder.php @@ -4,7 +4,6 @@ use Illuminate\Console\Command; use Illuminate\Container\Container; -use Illuminate\Database\Console\Seeds\WithoutModelEvents; use Illuminate\Support\Arr; use Illuminate\Support\Hookable; use InvalidArgumentException; @@ -174,7 +173,7 @@ public function __invoke(array $parameters = []) throw new InvalidArgumentException('Method [run] missing from '.get_class($this)); } - return $this->runHooks('invoke', $this, function() use ($parameters) { + return $this->runHooks('invoke', $this, function () use ($parameters) { return isset($this->container) ? $this->container->call([$this, 'run'], $parameters) : $this->run(...$parameters); diff --git a/src/Illuminate/Support/Hooks/Hook.php b/src/Illuminate/Support/Hooks/Hook.php index 249d22a08433..ac140d4f56de 100644 --- a/src/Illuminate/Support/Hooks/Hook.php +++ b/src/Illuminate/Support/Hooks/Hook.php @@ -51,7 +51,7 @@ public static function lowPriority(string $name, Closure $callback): Hook } /** - * Constructor + * Constructor. * * @param string $name * @param \Closure $callback @@ -62,7 +62,8 @@ public function __construct( public string $name, public Closure $callback, public int $priority = HookContract::PRIORITY_NORMAL - ) { } + ) { + } /** * @inheritdoc diff --git a/src/Illuminate/Support/Hooks/HookCollection.php b/src/Illuminate/Support/Hooks/HookCollection.php index a4dfdc0b825b..7c2e2dd3352e 100644 --- a/src/Illuminate/Support/Hooks/HookCollection.php +++ b/src/Illuminate/Support/Hooks/HookCollection.php @@ -54,7 +54,7 @@ public static function clearCache() protected static function loadHooks($class) { return collect((new ReflectionClass($class))->getMethods()) - ->map(fn($method) => static::hookForMethod($method)) + ->map(fn ($method) => static::hookForMethod($method)) ->filter(); } @@ -67,7 +67,7 @@ protected static function loadHooks($class) protected static function hookForMethod(ReflectionMethod $method): ?PendingHook { if (static::methodReturnsHook($method)) { - return new PendingHook(static function($instance = null) use ($method) { + return new PendingHook(static function ($instance = null) use ($method) { return $method->invoke($instance); }, $method->isStatic()); } @@ -105,13 +105,12 @@ public function run($name, $instance, $arguments = [], Closure $callback = null) ->where('name', $name) ->sortBy('priority'); - $hooks->each(fn(Hook $hook) => $hook->run($instance, $arguments)); + $hooks->each(fn (Hook $hook) => $hook->run($instance, $arguments)); try { return $callback ? $callback() : null; - } - finally { - $hooks->reverse()->each(fn(Hook $hook) => $hook->cleanup($instance, $arguments)); + } finally { + $hooks->reverse()->each(fn (Hook $hook) => $hook->cleanup($instance, $arguments)); } } diff --git a/src/Illuminate/Support/Hooks/PendingHook.php b/src/Illuminate/Support/Hooks/PendingHook.php index 8c4c0748b257..b7c2f487e818 100644 --- a/src/Illuminate/Support/Hooks/PendingHook.php +++ b/src/Illuminate/Support/Hooks/PendingHook.php @@ -24,7 +24,8 @@ class PendingHook public function __construct( public Closure $callback, public bool $isStatic - ) { } + ) { + } /** * Resolve the pending hook into a Hook instance. diff --git a/src/Illuminate/Support/Hooks/TraitHook.php b/src/Illuminate/Support/Hooks/TraitHook.php index 26b8b7d698da..76c0364e591c 100644 --- a/src/Illuminate/Support/Hooks/TraitHook.php +++ b/src/Illuminate/Support/Hooks/TraitHook.php @@ -7,7 +7,7 @@ class TraitHook implements HookContract { /** - * Constructor + * Constructor. * * @param string $prefix * @param int $priority From 2ce75fc414c3b8643febb4e6f9b0483acdf88be0 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 8 Dec 2021 16:21:11 -0500 Subject: [PATCH 06/19] Fix static closures --- src/Illuminate/Database/Console/Seeds/WithoutModelEvents.php | 4 ++-- src/Illuminate/Database/Seeder.php | 2 +- src/Illuminate/Support/Hooks/HookCollection.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Database/Console/Seeds/WithoutModelEvents.php b/src/Illuminate/Database/Console/Seeds/WithoutModelEvents.php index 1fcb922c4a63..cc4e344395df 100644 --- a/src/Illuminate/Database/Console/Seeds/WithoutModelEvents.php +++ b/src/Illuminate/Database/Console/Seeds/WithoutModelEvents.php @@ -15,14 +15,14 @@ trait WithoutModelEvents */ public static function withoutModelEvents(): Hook { - return Hook::make('invoke', static function () { + return Hook::make('run', function () { if (! $dispatcher = Model::getEventDispatcher()) { return null; } Model::setEventDispatcher(new NullDispatcher($dispatcher)); - return static fn () => Model::setEventDispatcher($dispatcher); + return fn () => Model::setEventDispatcher($dispatcher); }); } } diff --git a/src/Illuminate/Database/Seeder.php b/src/Illuminate/Database/Seeder.php index be750166cf52..bcdac810ccb9 100755 --- a/src/Illuminate/Database/Seeder.php +++ b/src/Illuminate/Database/Seeder.php @@ -173,7 +173,7 @@ public function __invoke(array $parameters = []) throw new InvalidArgumentException('Method [run] missing from '.get_class($this)); } - return $this->runHooks('invoke', $this, function () use ($parameters) { + return $this->runHooks('run', $this, function () use ($parameters) { return isset($this->container) ? $this->container->call([$this, 'run'], $parameters) : $this->run(...$parameters); diff --git a/src/Illuminate/Support/Hooks/HookCollection.php b/src/Illuminate/Support/Hooks/HookCollection.php index 7c2e2dd3352e..702544a38b7f 100644 --- a/src/Illuminate/Support/Hooks/HookCollection.php +++ b/src/Illuminate/Support/Hooks/HookCollection.php @@ -67,7 +67,7 @@ protected static function loadHooks($class) protected static function hookForMethod(ReflectionMethod $method): ?PendingHook { if (static::methodReturnsHook($method)) { - return new PendingHook(static function ($instance = null) use ($method) { + return new PendingHook(function ($instance = null) use ($method) { return $method->invoke($instance); }, $method->isStatic()); } From 40a91568c7a2d3cc001a15a07e801de79cd057cd Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 8 Dec 2021 16:50:44 -0500 Subject: [PATCH 07/19] Implement hook support on Models --- src/Illuminate/Contracts/Support/Hook.php | 14 +++++ src/Illuminate/Database/Eloquent/Model.php | 54 +++++++++---------- src/Illuminate/Support/Hooks/Hook.php | 16 ++++++ .../Support/Hooks/HookCollection.php | 5 +- src/Illuminate/Support/Hooks/PendingHook.php | 1 + src/Illuminate/Support/Hooks/TraitHook.php | 16 ++++++ 6 files changed, 76 insertions(+), 30 deletions(-) diff --git a/src/Illuminate/Contracts/Support/Hook.php b/src/Illuminate/Contracts/Support/Hook.php index 4e18c7b5dfe8..f9b214926702 100644 --- a/src/Illuminate/Contracts/Support/Hook.php +++ b/src/Illuminate/Contracts/Support/Hook.php @@ -40,4 +40,18 @@ public function run($instance, array $arguments = []); * @param array $arguments */ public function cleanup($instance, array $arguments = []); + + /** + * Get the name of the hook. + * + * @return string + */ + public function getName(); + + /** + * Get the priority of the hook. + * + * @return int + */ + public function getPriority(); } diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index e0a36f465447..f00aef84ca34 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -18,6 +18,8 @@ use Illuminate\Database\Eloquent\Relations\Pivot; use Illuminate\Support\Arr; use Illuminate\Support\Collection as BaseCollection; +use Illuminate\Support\Hookable; +use Illuminate\Support\Hooks\TraitHook; use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; use JsonSerializable; @@ -32,7 +34,8 @@ abstract class Model implements Arrayable, ArrayAccess, CanBeEscapedWhenCastToSt Concerns\HasTimestamps, Concerns\HidesAttributes, Concerns\GuardsAttributes, - ForwardsCalls; + ForwardsCalls, + Hookable; /** * The connection name for the model. @@ -252,6 +255,26 @@ protected static function boot() static::bootTraits(); } + /** + * Register hook for `bootX` trait methods. + * + * @return \Illuminate\Support\Hooks\TraitHook + */ + public static function registerBootHook(): TraitHook + { + return new TraitHook('boot'); + } + + /** + * Register hook for `initializeX` trait methods. + * + * @return \Illuminate\Support\Hooks\TraitHook + */ + public static function registerInitializeHook(): TraitHook + { + return new TraitHook('initialize'); + } + /** * Boot all of the bootable traits on the model. * @@ -259,30 +282,7 @@ protected static function boot() */ protected static function bootTraits() { - $class = static::class; - - $booted = []; - - static::$traitInitializers[$class] = []; - - // TODO: Switch to Hookable - foreach (class_uses_recursive($class) as $trait) { - $method = 'boot'.class_basename($trait); - - if (method_exists($class, $method) && ! in_array($method, $booted)) { - forward_static_call([$class, $method]); - - $booted[] = $method; - } - - if (method_exists($class, $method = 'initialize'.class_basename($trait))) { - static::$traitInitializers[$class][] = $method; - - static::$traitInitializers[$class] = array_unique( - static::$traitInitializers[$class] - ); - } - } + static::runStaticHooks('boot'); } /** @@ -292,9 +292,7 @@ protected static function bootTraits() */ protected function initializeTraits() { - foreach (static::$traitInitializers[static::class] as $method) { - $this->{$method}(); - } + $this->runHooks('initialize'); } /** diff --git a/src/Illuminate/Support/Hooks/Hook.php b/src/Illuminate/Support/Hooks/Hook.php index ac140d4f56de..6ad3f48daa7d 100644 --- a/src/Illuminate/Support/Hooks/Hook.php +++ b/src/Illuminate/Support/Hooks/Hook.php @@ -91,6 +91,22 @@ public function cleanup($instance, array $arguments = []) $this->runCallback($this->cleanup, $instance, $arguments); } + /** + * @inheritdoc + */ + public function getName() + { + return $this->name; + } + + /** + * @inheritDoc + */ + public function getPriority() + { + return $this->priority; + } + /** * Run a callback that may or may not be static. * diff --git a/src/Illuminate/Support/Hooks/HookCollection.php b/src/Illuminate/Support/Hooks/HookCollection.php index 702544a38b7f..4f21365af423 100644 --- a/src/Illuminate/Support/Hooks/HookCollection.php +++ b/src/Illuminate/Support/Hooks/HookCollection.php @@ -3,6 +3,7 @@ namespace Illuminate\Support\Hooks; use Closure; +use Illuminate\Contracts\Support\Hook; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use ReflectionClass; @@ -102,8 +103,8 @@ public function run($name, $instance, $arguments = [], Closure $callback = null) $hooks = $this->onlyStatic(! is_object($instance)) ->resolve($instance) - ->where('name', $name) - ->sortBy('priority'); + ->filter(fn(Hook $hook) => $hook->getName() === $name) + ->sortBy(fn(Hook $hook) => $hook->getPriority()); $hooks->each(fn (Hook $hook) => $hook->run($instance, $arguments)); diff --git a/src/Illuminate/Support/Hooks/PendingHook.php b/src/Illuminate/Support/Hooks/PendingHook.php index b7c2f487e818..e4d88e0fb009 100644 --- a/src/Illuminate/Support/Hooks/PendingHook.php +++ b/src/Illuminate/Support/Hooks/PendingHook.php @@ -3,6 +3,7 @@ namespace Illuminate\Support\Hooks; use Closure; +use Illuminate\Contracts\Support\Hook; use RuntimeException; class PendingHook diff --git a/src/Illuminate/Support/Hooks/TraitHook.php b/src/Illuminate/Support/Hooks/TraitHook.php index 76c0364e591c..f590b9a7e610 100644 --- a/src/Illuminate/Support/Hooks/TraitHook.php +++ b/src/Illuminate/Support/Hooks/TraitHook.php @@ -45,4 +45,20 @@ public function cleanup($instance, array $arguments = []) { // No cleanup is necessary } + + /** + * @inheritdoc + */ + public function getName() + { + return $this->prefix; + } + + /** + * @inheritDoc + */ + public function getPriority() + { + return $this->priority; + } } From 1301cd5877ce8515d4dda3eddb2a5b8d20e2a9dc Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 8 Dec 2021 16:51:16 -0500 Subject: [PATCH 08/19] Code style --- src/Illuminate/Support/Hooks/HookCollection.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Support/Hooks/HookCollection.php b/src/Illuminate/Support/Hooks/HookCollection.php index 4f21365af423..c17bf55975aa 100644 --- a/src/Illuminate/Support/Hooks/HookCollection.php +++ b/src/Illuminate/Support/Hooks/HookCollection.php @@ -103,8 +103,8 @@ public function run($name, $instance, $arguments = [], Closure $callback = null) $hooks = $this->onlyStatic(! is_object($instance)) ->resolve($instance) - ->filter(fn(Hook $hook) => $hook->getName() === $name) - ->sortBy(fn(Hook $hook) => $hook->getPriority()); + ->filter(fn (Hook $hook) => $hook->getName() === $name) + ->sortBy(fn (Hook $hook) => $hook->getPriority()); $hooks->each(fn (Hook $hook) => $hook->run($instance, $arguments)); From a4eda86d32f52986320b34f012d19eeaa0713810 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 8 Dec 2021 17:06:39 -0500 Subject: [PATCH 09/19] Handle PHPUnit mock objects (not ideal) --- src/Illuminate/Support/Hooks/PendingHook.php | 32 ++++++++++++++++++-- 1 file changed, 29 insertions(+), 3 deletions(-) diff --git a/src/Illuminate/Support/Hooks/PendingHook.php b/src/Illuminate/Support/Hooks/PendingHook.php index e4d88e0fb009..80205459af0f 100644 --- a/src/Illuminate/Support/Hooks/PendingHook.php +++ b/src/Illuminate/Support/Hooks/PendingHook.php @@ -4,6 +4,8 @@ use Closure; use Illuminate\Contracts\Support\Hook; +use Illuminate\Support\Str; +use PHPUnit\Framework\MockObject\BadMethodCallException; use RuntimeException; class PendingHook @@ -52,9 +54,33 @@ public function resolve($instance = null) protected function getHook($instance = null): Hook { if (is_null($this->hook)) { - $this->hook = $this->isStatic - ? call_user_func($this->callback) - : $this->callback->call($instance); + try { + $this->hook = $this->isStatic + ? call_user_func($this->callback) + : $this->callback->call($instance); + } catch (BadMethodCallException $exception) { + $this->hook = new class implements Hook { + public function run($instance, array $arguments = []) + { + throw new RuntimeException('Unexpected hook call from mock object'); + } + + public function cleanup($instance, array $arguments = []) + { + throw new RuntimeException('Unexpected hook call from mock object'); + } + + public function getName() + { + return Str::random(); + } + + public function getPriority() + { + return PHP_INT_MAX; + } + }; + } } return $this->hook; From a46c19c33d66e375455e409901f6b6a6970d03b1 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Wed, 8 Dec 2021 17:10:21 -0500 Subject: [PATCH 10/19] Code style --- src/Illuminate/Support/Hooks/PendingHook.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Illuminate/Support/Hooks/PendingHook.php b/src/Illuminate/Support/Hooks/PendingHook.php index 80205459af0f..2d4c19c85a41 100644 --- a/src/Illuminate/Support/Hooks/PendingHook.php +++ b/src/Illuminate/Support/Hooks/PendingHook.php @@ -59,7 +59,8 @@ protected function getHook($instance = null): Hook ? call_user_func($this->callback) : $this->callback->call($instance); } catch (BadMethodCallException $exception) { - $this->hook = new class implements Hook { + $this->hook = new class implements Hook + { public function run($instance, array $arguments = []) { throw new RuntimeException('Unexpected hook call from mock object'); From 9a4bfd43e3abfbd80399acf3bec62831a002ecb2 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Thu, 9 Dec 2021 11:36:35 -0500 Subject: [PATCH 11/19] Add hooks to GeneratorCommand --- .../Console/Concerns/CreatesMatchingTest.php | 50 ++++++++++++------- src/Illuminate/Console/GeneratorCommand.php | 23 ++++----- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php index a360c281a98a..f72e0b912818 100644 --- a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php +++ b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php @@ -2,43 +2,55 @@ namespace Illuminate\Console\Concerns; +use Illuminate\Support\Hooks\Hook; use Illuminate\Support\Str; use Symfony\Component\Console\Input\InputOption; trait CreatesMatchingTest { /** - * Add the standard command options for generating matching tests. + * Hook to add the standard command options for generating matching tests. * * @return void */ - protected function addTestOptions() + public function addTestOptionsHook(): Hook { - foreach (['test' => 'PHPUnit', 'pest' => 'Pest'] as $option => $name) { - $this->getDefinition()->addOption(new InputOption( - $option, - null, - InputOption::VALUE_NONE, - "Generate an accompanying {$name} test for the {$this->type}" - )); - } + return Hook::make('initialize', function() { + foreach (['test' => 'PHPUnit', 'pest' => 'Pest'] as $option => $name) { + $this->getDefinition()->addOption(new InputOption( + $option, + null, + InputOption::VALUE_NONE, + "Generate an accompanying {$name} test for the {$this->type}" + )); + } + }); } /** - * Create the matching test case if requested. + * Hook tocreate the matching test case if requested. * * @param string $path * @return void */ - protected function handleTestCreation($path) + public function addTestCreationHook($path): Hook { - if (! $this->option('test') && ! $this->option('pest')) { - return; - } + return Hook::make('generate', function() use ($path) { + if (! $this->option('test') && ! $this->option('pest')) { + return; + } - $this->call('make:test', [ - 'name' => Str::of($path)->after($this->laravel['path'])->beforeLast('.php')->append('Test')->replace('\\', '/'), - '--pest' => $this->option('pest'), - ]); + // Run make:test after the "generate" hook has finished + return function() use ($path) { + $this->call('make:test', [ + 'name' => Str::of($path) + ->after($this->laravel['path']) + ->beforeLast('.php') + ->append('Test') + ->replace('\\', '/'), + '--pest' => $this->option('pest'), + ]); + }; + }); } } diff --git a/src/Illuminate/Console/GeneratorCommand.php b/src/Illuminate/Console/GeneratorCommand.php index 7d7b9609fe5c..bd91738fc5cc 100644 --- a/src/Illuminate/Console/GeneratorCommand.php +++ b/src/Illuminate/Console/GeneratorCommand.php @@ -4,11 +4,14 @@ use Illuminate\Console\Concerns\CreatesMatchingTest; use Illuminate\Filesystem\Filesystem; +use Illuminate\Support\Hookable; use Illuminate\Support\Str; use Symfony\Component\Console\Input\InputArgument; abstract class GeneratorCommand extends Command { + use Hookable; + /** * The filesystem instance. * @@ -109,10 +112,7 @@ public function __construct(Filesystem $files) { parent::__construct(); - // TODO: Switch to Hookable - if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) { - $this->addTestOptions(); - } + $this->runHooks('initialize'); $this->files = $files; } @@ -157,19 +157,16 @@ public function handle() return false; } - // Next, we will generate the path to the location where this class' file should get + // Finally, we will generate the path to the location where this class' file should get // written. Then, we will build the class and make the proper replacements on the // stub files so that it gets the correctly formatted namespace and class name. - $this->makeDirectory($path); + $this->runHooks('generate', [$name, $path], function () use ($name, $path) { + $this->makeDirectory($path); - $this->files->put($path, $this->sortImports($this->buildClass($name))); + $this->files->put($path, $this->sortImports($this->buildClass($name))); - $this->info($this->type.' created successfully.'); - - // TODO: Switch to Hookable - if (in_array(CreatesMatchingTest::class, class_uses_recursive($this))) { - $this->handleTestCreation($path); - } + $this->info($this->type.' created successfully.'); + }); } /** From 5e29880aa79f93b5528281e4f064a9158c2e87fa Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Thu, 9 Dec 2021 11:37:30 -0500 Subject: [PATCH 12/19] StyleCI --- src/Illuminate/Console/Concerns/CreatesMatchingTest.php | 6 +++--- src/Illuminate/Console/GeneratorCommand.php | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php index f72e0b912818..4a2c3bc7566d 100644 --- a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php +++ b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php @@ -15,7 +15,7 @@ trait CreatesMatchingTest */ public function addTestOptionsHook(): Hook { - return Hook::make('initialize', function() { + return Hook::make('initialize', function () { foreach (['test' => 'PHPUnit', 'pest' => 'Pest'] as $option => $name) { $this->getDefinition()->addOption(new InputOption( $option, @@ -35,13 +35,13 @@ public function addTestOptionsHook(): Hook */ public function addTestCreationHook($path): Hook { - return Hook::make('generate', function() use ($path) { + return Hook::make('generate', function () use ($path) { if (! $this->option('test') && ! $this->option('pest')) { return; } // Run make:test after the "generate" hook has finished - return function() use ($path) { + return function () use ($path) { $this->call('make:test', [ 'name' => Str::of($path) ->after($this->laravel['path']) diff --git a/src/Illuminate/Console/GeneratorCommand.php b/src/Illuminate/Console/GeneratorCommand.php index bd91738fc5cc..431104d97f76 100644 --- a/src/Illuminate/Console/GeneratorCommand.php +++ b/src/Illuminate/Console/GeneratorCommand.php @@ -2,7 +2,6 @@ namespace Illuminate\Console; -use Illuminate\Console\Concerns\CreatesMatchingTest; use Illuminate\Filesystem\Filesystem; use Illuminate\Support\Hookable; use Illuminate\Support\Str; From eacd37829c8c1664b3857b85a65d9fa2b7482e96 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Thu, 9 Dec 2021 11:46:34 -0500 Subject: [PATCH 13/19] Swap TestCase::setUpTraits for hooks --- .../Foundation/Testing/DatabaseMigrations.php | 11 +++++ .../Testing/DatabaseTransactions.php | 12 ++++++ .../Foundation/Testing/RefreshDatabase.php | 11 +++++ .../Foundation/Testing/TestCase.php | 43 ++----------------- .../Foundation/Testing/WithFaker.php | 11 +++++ .../Foundation/Testing/WithoutEvents.php | 11 +++++ .../Foundation/Testing/WithoutMiddleware.php | 11 +++++ 7 files changed, 71 insertions(+), 39 deletions(-) diff --git a/src/Illuminate/Foundation/Testing/DatabaseMigrations.php b/src/Illuminate/Foundation/Testing/DatabaseMigrations.php index 10a3a7300af6..75eaa37d6141 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseMigrations.php +++ b/src/Illuminate/Foundation/Testing/DatabaseMigrations.php @@ -4,11 +4,22 @@ use Illuminate\Contracts\Console\Kernel; use Illuminate\Foundation\Testing\Traits\CanConfigureMigrationCommands; +use Illuminate\Support\Hooks\Hook; trait DatabaseMigrations { use CanConfigureMigrationCommands; + /** + * Register test case hook. + * + * @return \Illuminate\Support\Hooks\Hook + */ + public function registerDatabaseMigrationsHook(): Hook + { + return new Hook('setUp', fn () => $this->runDatabaseMigrations(), 55); + } + /** * Define hooks to migrate the database before and after each test. * diff --git a/src/Illuminate/Foundation/Testing/DatabaseTransactions.php b/src/Illuminate/Foundation/Testing/DatabaseTransactions.php index e162e188a4ed..97e695162f8f 100644 --- a/src/Illuminate/Foundation/Testing/DatabaseTransactions.php +++ b/src/Illuminate/Foundation/Testing/DatabaseTransactions.php @@ -2,8 +2,20 @@ namespace Illuminate\Foundation\Testing; +use Illuminate\Support\Hooks\Hook; + trait DatabaseTransactions { + /** + * Register test case hook. + * + * @return \Illuminate\Support\Hooks\Hook + */ + public function registerDatabaseTransactionsHook(): Hook + { + return new Hook('setUp', fn () => $this->beginDatabaseTransaction(), 60); + } + /** * Handle database transactions on the specified connections. * diff --git a/src/Illuminate/Foundation/Testing/RefreshDatabase.php b/src/Illuminate/Foundation/Testing/RefreshDatabase.php index 55ff86d965d6..8335cd357420 100644 --- a/src/Illuminate/Foundation/Testing/RefreshDatabase.php +++ b/src/Illuminate/Foundation/Testing/RefreshDatabase.php @@ -4,11 +4,22 @@ use Illuminate\Contracts\Console\Kernel; use Illuminate\Foundation\Testing\Traits\CanConfigureMigrationCommands; +use Illuminate\Support\Hooks\Hook; trait RefreshDatabase { use CanConfigureMigrationCommands; + /** + * Register test case hook. + * + * @return \Illuminate\Support\Hooks\Hook + */ + public function registerRefreshDatabaseHook(): Hook + { + return new Hook('setUp', fn () => $this->refreshDatabase(), 50); + } + /** * Define hooks to migrate the database before and after each test. * diff --git a/src/Illuminate/Foundation/Testing/TestCase.php b/src/Illuminate/Foundation/Testing/TestCase.php index 24b7cd77aca2..cdf1a52645d4 100644 --- a/src/Illuminate/Foundation/Testing/TestCase.php +++ b/src/Illuminate/Foundation/Testing/TestCase.php @@ -9,6 +9,7 @@ use Illuminate\Support\Carbon; use Illuminate\Support\Facades\Facade; use Illuminate\Support\Facades\ParallelTesting; +use Illuminate\Support\Hookable; use Illuminate\Support\Str; use Mockery; use Mockery\Exception\InvalidCountException; @@ -27,7 +28,8 @@ abstract class TestCase extends BaseTestCase Concerns\InteractsWithSession, Concerns\InteractsWithTime, Concerns\InteractsWithViews, - Concerns\MocksApplicationServices; + Concerns\MocksApplicationServices, + Hookable; /** * The Illuminate application instance. @@ -88,7 +90,7 @@ protected function setUp(): void ParallelTesting::callSetUpTestCaseCallbacks($this); } - $this->setUpTraits(); + $this->runHooks('setUp'); foreach ($this->afterApplicationCreatedCallbacks as $callback) { $callback(); @@ -109,43 +111,6 @@ protected function refreshApplication() $this->app = $this->createApplication(); } - /** - * Boot the testing helper traits. - * - * @return array - */ - protected function setUpTraits() - { - // TODO: Switch to Hookable - $uses = array_flip(class_uses_recursive(static::class)); - - if (isset($uses[RefreshDatabase::class])) { - $this->refreshDatabase(); - } - - if (isset($uses[DatabaseMigrations::class])) { - $this->runDatabaseMigrations(); - } - - if (isset($uses[DatabaseTransactions::class])) { - $this->beginDatabaseTransaction(); - } - - if (isset($uses[WithoutMiddleware::class])) { - $this->disableMiddlewareForAllTests(); - } - - if (isset($uses[WithoutEvents::class])) { - $this->disableEventsForAllTests(); - } - - if (isset($uses[WithFaker::class])) { - $this->setUpFaker(); - } - - return $uses; - } - /** * Clean up the testing environment before the next test. * diff --git a/src/Illuminate/Foundation/Testing/WithFaker.php b/src/Illuminate/Foundation/Testing/WithFaker.php index cd276fbd4eb0..9799813914f7 100644 --- a/src/Illuminate/Foundation/Testing/WithFaker.php +++ b/src/Illuminate/Foundation/Testing/WithFaker.php @@ -4,6 +4,7 @@ use Faker\Factory; use Faker\Generator; +use Illuminate\Support\Hooks\Hook; trait WithFaker { @@ -14,6 +15,16 @@ trait WithFaker */ protected $faker; + /** + * Register test case hook. + * + * @return \Illuminate\Support\Hooks\Hook + */ + public function registerWithFakerHook(): Hook + { + return new Hook('setUp', fn () => $this->setUpFaker(), 75); + } + /** * Setup up the Faker instance. * diff --git a/src/Illuminate/Foundation/Testing/WithoutEvents.php b/src/Illuminate/Foundation/Testing/WithoutEvents.php index fa5df3ce8f5b..bc9652416f7a 100644 --- a/src/Illuminate/Foundation/Testing/WithoutEvents.php +++ b/src/Illuminate/Foundation/Testing/WithoutEvents.php @@ -3,9 +3,20 @@ namespace Illuminate\Foundation\Testing; use Exception; +use Illuminate\Support\Hooks\Hook; trait WithoutEvents { + /** + * Register test case hook. + * + * @return \Illuminate\Support\Hooks\Hook + */ + public function registerWithoutEventsHook(): Hook + { + return new Hook('setUp', fn () => $this->disableEventsForAllTests(), 70); + } + /** * Prevent all event handles from being executed. * diff --git a/src/Illuminate/Foundation/Testing/WithoutMiddleware.php b/src/Illuminate/Foundation/Testing/WithoutMiddleware.php index 269b532d3150..ebb5a9331dcd 100644 --- a/src/Illuminate/Foundation/Testing/WithoutMiddleware.php +++ b/src/Illuminate/Foundation/Testing/WithoutMiddleware.php @@ -3,9 +3,20 @@ namespace Illuminate\Foundation\Testing; use Exception; +use Illuminate\Support\Hooks\Hook; trait WithoutMiddleware { + /** + * Register test case hook. + * + * @return \Illuminate\Support\Hooks\Hook + */ + public function registerWithoutMiddlewareHook(): Hook + { + return new Hook('setUp', fn () => $this->disableMiddlewareForAllTests(), 65); + } + /** * Prevent all middleware from being executed for this test class. * From 132599fef1546fa6e6c3775ad7bc7f51e8bad9f5 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Thu, 9 Dec 2021 11:53:04 -0500 Subject: [PATCH 14/19] A little clean up around conventions-based hooks --- src/Illuminate/Database/Eloquent/Model.php | 18 ++++++------- src/Illuminate/Support/Hookable.php | 25 ------------------- .../{TraitHook.php => ConventionalHook.php} | 8 ++++-- 3 files changed, 15 insertions(+), 36 deletions(-) rename src/Illuminate/Support/Hooks/{TraitHook.php => ConventionalHook.php} (86%) diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index f00aef84ca34..c134fd3af876 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -19,7 +19,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Collection as BaseCollection; use Illuminate\Support\Hookable; -use Illuminate\Support\Hooks\TraitHook; +use Illuminate\Support\Hooks\ConventionalHook; use Illuminate\Support\Str; use Illuminate\Support\Traits\ForwardsCalls; use JsonSerializable; @@ -256,23 +256,23 @@ protected static function boot() } /** - * Register hook for `bootX` trait methods. + * Register a conventions-based hook for boot[trait name] methods. * - * @return \Illuminate\Support\Hooks\TraitHook + * @return \Illuminate\Support\Hooks\ConventionalHook */ - public static function registerBootHook(): TraitHook + public static function registerBootHook(): ConventionalHook { - return new TraitHook('boot'); + return new ConventionalHook('boot'); } /** - * Register hook for `initializeX` trait methods. + * Register a conventions-based hook for initialize[trait name] methods. * - * @return \Illuminate\Support\Hooks\TraitHook + * @return \Illuminate\Support\Hooks\ConventionalHook */ - public static function registerInitializeHook(): TraitHook + public static function registerInitializeHook(): ConventionalHook { - return new TraitHook('initialize'); + return new ConventionalHook('initialize'); } /** diff --git a/src/Illuminate/Support/Hookable.php b/src/Illuminate/Support/Hookable.php index f776dea76d8c..db97641ff7b9 100644 --- a/src/Illuminate/Support/Hookable.php +++ b/src/Illuminate/Support/Hookable.php @@ -4,7 +4,6 @@ use Closure; use Illuminate\Support\Hooks\HookCollection; -use Illuminate\Support\Hooks\TraitHook; trait Hookable { @@ -21,18 +20,6 @@ protected static function runStaticHooks($name, $arguments = [], Closure $callba return HookCollection::for(static::class)->run($name, static::class, $arguments, $callback); } - /** - * Run trait hooks statically. - * - * @param string $prefix - * @param array $arguments - * @return void - */ - protected static function runStaticTraitHooks($prefix, $arguments = []) - { - (new TraitHook($prefix))->run(static::class, $arguments); - } - /** * Run hooks non-statically. * @@ -45,16 +32,4 @@ protected function runHooks($name, $arguments = [], Closure $callback = null) { return HookCollection::for(static::class)->run($name, $this, $arguments, $callback); } - - /** - * Run trait hooks. - * - * @param string $prefix - * @param array $arguments - * @return void - */ - protected function runTraitHooks($prefix, $arguments = []) - { - (new TraitHook($prefix))->run($this, $arguments); - } } diff --git a/src/Illuminate/Support/Hooks/TraitHook.php b/src/Illuminate/Support/Hooks/ConventionalHook.php similarity index 86% rename from src/Illuminate/Support/Hooks/TraitHook.php rename to src/Illuminate/Support/Hooks/ConventionalHook.php index f590b9a7e610..707950ba6c7d 100644 --- a/src/Illuminate/Support/Hooks/TraitHook.php +++ b/src/Illuminate/Support/Hooks/ConventionalHook.php @@ -4,7 +4,7 @@ use Illuminate\Contracts\Support\Hook as HookContract; -class TraitHook implements HookContract +class ConventionalHook implements HookContract { /** * Constructor. @@ -25,15 +25,19 @@ public function __construct( */ public function run($instance = null, array $arguments = []) { + $executed = []; + foreach (class_uses_recursive($instance) as $trait) { $method = $this->prefix.class_basename($trait); - if (method_exists($instance, $method)) { + if (! in_array($method, $executed) && method_exists($instance, $method)) { if (is_object($instance)) { $instance->$method(...$arguments); } else { forward_static_call_array([$instance, $method], $arguments); } + + $executed[] = $method; } } } From 4ee4dfcf6c02388f4e0ad00776dc9e886f11a243 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Thu, 9 Dec 2021 12:00:10 -0500 Subject: [PATCH 15/19] Improve backwards-compatibility --- .../Console/Concerns/CreatesMatchingTest.php | 76 +++++++++++-------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php index 4a2c3bc7566d..b68a5219f6a8 100644 --- a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php +++ b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php @@ -9,48 +9,64 @@ trait CreatesMatchingTest { /** - * Hook to add the standard command options for generating matching tests. + * Register "initialize" hook * - * @return void + * @return \Illuminate\Support\Hooks\Hook + */ + public function registerCreatesMatchingTestInitializeHook(): Hook + { + return Hook::make('initialize', fn () => $this->addTestOptions()); + } + + /** + * Register "generate" hook + * + * @return \Illuminate\Support\Hooks\Hook */ - public function addTestOptionsHook(): Hook + public function registerCreatesMatchingTestGenerateHook(): Hook { - return Hook::make('initialize', function () { - foreach (['test' => 'PHPUnit', 'pest' => 'Pest'] as $option => $name) { - $this->getDefinition()->addOption(new InputOption( - $option, - null, - InputOption::VALUE_NONE, - "Generate an accompanying {$name} test for the {$this->type}" - )); - } + return Hook::make('generate', function ($name, $path) { + // We want to run test creation after generation, so we'll return a callback to execute at the end + return fn() => $this->handleTestCreation($path); }); } /** - * Hook tocreate the matching test case if requested. + * Add the standard command options for generating matching tests. + * + * @return void + */ + protected function addTestOptions() + { + foreach (['test' => 'PHPUnit', 'pest' => 'Pest'] as $option => $name) { + $this->getDefinition()->addOption(new InputOption( + $option, + null, + InputOption::VALUE_NONE, + "Generate an accompanying {$name} test for the {$this->type}" + )); + } + } + + /** + * Create the matching test case if requested. * * @param string $path * @return void */ - public function addTestCreationHook($path): Hook + protected function handleTestCreation($path) { - return Hook::make('generate', function () use ($path) { - if (! $this->option('test') && ! $this->option('pest')) { - return; - } + if (! $this->option('test') && ! $this->option('pest')) { + return; + } - // Run make:test after the "generate" hook has finished - return function () use ($path) { - $this->call('make:test', [ - 'name' => Str::of($path) - ->after($this->laravel['path']) - ->beforeLast('.php') - ->append('Test') - ->replace('\\', '/'), - '--pest' => $this->option('pest'), - ]); - }; - }); + $this->call('make:test', [ + 'name' => Str::of($path) + ->after($this->laravel['path']) + ->beforeLast('.php') + ->append('Test') + ->replace('\\', '/'), + '--pest' => $this->option('pest'), + ]); } } From 3e50f9b3dc30b79d8d8dfa5806e6fc2fe162f1e0 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Thu, 9 Dec 2021 12:00:45 -0500 Subject: [PATCH 16/19] Code style --- src/Illuminate/Console/Concerns/CreatesMatchingTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php index b68a5219f6a8..2290e25183e2 100644 --- a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php +++ b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php @@ -27,7 +27,7 @@ public function registerCreatesMatchingTestGenerateHook(): Hook { return Hook::make('generate', function ($name, $path) { // We want to run test creation after generation, so we'll return a callback to execute at the end - return fn() => $this->handleTestCreation($path); + return fn () => $this->handleTestCreation($path); }); } From 3b0be8392051dc69107dc33443de1f79498e4118 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Thu, 9 Dec 2021 12:02:07 -0500 Subject: [PATCH 17/19] Make FS available to generator hooks --- src/Illuminate/Console/GeneratorCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Console/GeneratorCommand.php b/src/Illuminate/Console/GeneratorCommand.php index 431104d97f76..f81cc45bc427 100644 --- a/src/Illuminate/Console/GeneratorCommand.php +++ b/src/Illuminate/Console/GeneratorCommand.php @@ -111,9 +111,9 @@ public function __construct(Filesystem $files) { parent::__construct(); - $this->runHooks('initialize'); - $this->files = $files; + + $this->runHooks('initialize'); } /** From 1556ae35fa0eea853a2283540edbe9129dcbb7ee Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Thu, 9 Dec 2021 12:02:45 -0500 Subject: [PATCH 18/19] StyleCI --- src/Illuminate/Console/Concerns/CreatesMatchingTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php index 2290e25183e2..e51b04ad9d28 100644 --- a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php +++ b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php @@ -9,7 +9,7 @@ trait CreatesMatchingTest { /** - * Register "initialize" hook + * Register "initialize" hook. * * @return \Illuminate\Support\Hooks\Hook */ @@ -19,7 +19,7 @@ public function registerCreatesMatchingTestInitializeHook(): Hook } /** - * Register "generate" hook + * Register "generate" hook. * * @return \Illuminate\Support\Hooks\Hook */ From 2218a97ef7f7c1e412852e9b4fcb0811079a1ec7 Mon Sep 17 00:00:00 2001 From: Chris Morrell Date: Thu, 9 Dec 2021 12:04:20 -0500 Subject: [PATCH 19/19] Fix auto-applied code formatting --- src/Illuminate/Console/Concerns/CreatesMatchingTest.php | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php index e51b04ad9d28..862cce38a141 100644 --- a/src/Illuminate/Console/Concerns/CreatesMatchingTest.php +++ b/src/Illuminate/Console/Concerns/CreatesMatchingTest.php @@ -61,11 +61,7 @@ protected function handleTestCreation($path) } $this->call('make:test', [ - 'name' => Str::of($path) - ->after($this->laravel['path']) - ->beforeLast('.php') - ->append('Test') - ->replace('\\', '/'), + 'name' => Str::of($path)->after($this->laravel['path'])->beforeLast('.php')->append('Test')->replace('\\', '/'), '--pest' => $this->option('pest'), ]); }