src/HookTrait.php
<?php declare(strict_types=1); namespace Atk4\Core; trait HookTrait{Line exceeds 120 characters; contains 121 characters /** @var array<string, array<int, array<int<0, max>, array{\Closure, list<mixed>}>>> Configured hooks (callbacks). */ protected array $hooks = []; /** @var int<0, max> Next hook index counter. */Property name "$_hookIndexCounter" should not be prefixed with an underscore to indicate visibility private int $_hookIndexCounter = 0; /** @var \WeakReference<static>|null */Property name "$_hookOrigThis" should not be prefixed with an underscore to indicate visibility private ?\WeakReference $_hookOrigThis = null; /** * Optimize GC. When a Closure is guaranteed to be rebound before invoke, it can be rebound * to (deduplicated) fake instance before safely. */Avoid variables with short names like $fx. Configured minimum length is 3.
The variable $_instances is not named in camelCase.
The method _rebindHookFxToFakeInstance is not named in camelCase. private function _rebindHookFxToFakeInstance(\Closure $fx): \Closure {Missing class import via use statement (line '24', column '24'). $fxThis = (new \ReflectionFunction($fx))->getClosureThis(); Avoid excessively long variable names like $instanceWithoutConstructorCache. Keep variable name length under 20. $instanceWithoutConstructorCache = new class { /** @var array<class-string, object> */ private static array $_instances = []; /** * @param class-string $class */ public function getInstance(string $class): object { if (!isset(self::$_instances[$class])) {Missing class import via use statement (line '36', column '43'). $dummyInstance = (new \ReflectionClass($class))->newInstanceWithoutConstructor(); foreach ([$class, ...array_keys(class_parents($class))] as $scope) {Avoid using static access to class '\Closure' in method '_rebindHookFxToFakeInstance'. \Closure::bind(static function () use ($dummyInstance) { foreach (array_keys(get_object_vars($dummyInstance)) as $k) { unset($dummyInstance->{$k}); } }, null, $scope)(); } self::$_instances[$class] = $dummyInstance; } return self::$_instances[$class]; } }; $fakeThis = $instanceWithoutConstructorCache->getInstance(get_class($fxThis)); Avoid using static access to class '\Closure' in method '_rebindHookFxToFakeInstance'. return \Closure::bind($fx, $fakeThis); } /** * When hook Closure is bound to $this, rebinding all hooks after clone can be slow, optimize clone * by unbinding $this in favor of rebinding $this when hook is invoked. */Avoid variables with short names like $fx. Configured minimum length is 3.
The method _unbindHookFxIfBoundToThis is not named in camelCase. private function _unbindHookFxIfBoundToThis(\Closure $fx, bool $isShort): \Closure {Missing class import via use statement (line '62', column '24'). $fxThis = (new \ReflectionFunction($fx))->getClosureThis(); if ($fxThis !== $this) { return $fx; } $fx = $this->_rebindHookFxToFakeInstance($fx); return $this->_makeHookDynamicFx(null, $fx, $isShort); } The method _rebindHooksIfCloned is not named in camelCase. private function _rebindHooksIfCloned(): void { if ($this->_hookOrigThis !== null) { $hookOrigThis = $this->_hookOrigThis->get(); if ($hookOrigThis === $this) { return; } Avoid unused local variables such as '$spot'. foreach ($this->hooks as $spot => $hooksByPriority) {Avoid unused local variables such as '$priority'. foreach ($hooksByPriority as $priority => $hooksByIndex) {Avoid unused local variables such as '$index'. foreach ($hooksByIndex as $index => $hookData) {Missing class import via use statement (line '83', column '39'). $fxRefl = new \ReflectionFunction($hookData[0]); $fxThis = $fxRefl->getClosureThis(); if ($fxThis === null) { continue; } // TODO we throw only if the class name is the same, otherwise the check is too strict // and on a bad side - we should not throw when an object with a hook is cloned, // but instead we should throw once the closure this object is cloned // example of legit use: https://github.com/atk4/audit/blob/eb9810e085/src/Controller.php#L85Line exceeds 120 characters; contains 128 characters if (get_class($fxThis) === static::class || preg_match('~^Atk4\\\(?:Core|Data)~', get_class($fxThis))) {Line exceeds 120 characters; contains 124 characters throw (new Exception('Object cannot be cloned with hook bound to a different object than this')) ->addMoreInfo('closure_file', $fxRefl->getFileName()) ->addMoreInfo('closure_start_line', $fxRefl->getStartLine()); } } } } } Avoid using static access to class '\WeakReference' in method '_rebindHooksIfCloned'. $this->_hookOrigThis = \WeakReference::create($this); } /** * Add another callback to be executed during hook($spot);. * * Lower priority is called sooner. * * If priority is negative, then hook is prepended (executed first for the same priority). * * @param list<mixed> $args * * @return int<0, max> index under which the hook was added */Avoid variables with short names like $fx. Configured minimum length is 3. public function onHook(string $spot, \Closure $fx, array $args = [], int $priority = 5): int { $this->_rebindHooksIfCloned(); $fx = $this->_unbindHookFxIfBoundToThis($fx, false); $index = $this->_hookIndexCounter++; $data = [$fx, $args]; if ($priority < 0) { $this->hooks[$spot][$priority] = [$index => $data] + ($this->hooks[$spot][$priority] ?? []);The method onHook uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. } else { $this->hooks[$spot][$priority][$index] = $data; } return $index; } /** * Same as onHook() except no $this is passed to the callback as the 1st argument. * * @param list<mixed> $args * * @return int<0, max> index under which the hook was added */Avoid variables with short names like $fx. Configured minimum length is 3. public function onHookShort(string $spot, \Closure $fx, array $args = [], int $priority = 5): int { // create long callback and bind it to the same scope class and objectMissing class import via use statement (line '144', column '23'). $fxRefl = new \ReflectionFunction($fx); $fxScopeClassRefl = $fxRefl->getClosureScopeClass(); $fxThis = $fxRefl->getClosureThis(); if ($fxThis === null) {Avoid using static access to class '\Closure' in method 'onHookShort'. $fxLong = \Closure::bind(static function ($ignore, &...$args) use ($fx) { return $fx(...$args); }, null, $fxScopeClassRefl !== null ? $fxScopeClassRefl->getName() : null);The method onHookShort uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. } else { $fxLong = $this->_unbindHookFxIfBoundToThis($fx, true); if ($fxLong === $fx) { $fx = $this->_rebindHookFxToFakeInstance($fx); Avoid using static access to class '\Closure' in method 'onHookShort'.
Avoid unused parameters such as '$ignore'. $fxLong = \Closure::bind(function ($ignore, &...$args) use ($fx) {Avoid using static access to class '\Closure' in method 'onHookShort'. return \Closure::bind($fx, $this)(...$args); }, $fxThis, $fxScopeClassRefl->getName()); } } return $this->onHook($spot, $fxLong, $args, $priority); } /** * @param \Closure($this): object $getFxThisFx */Avoid variables with short names like $fx. Configured minimum length is 3.
The method _makeHookDynamicFx is not named in camelCase. private function _makeHookDynamicFx(?\Closure $getFxThisFx, \Closure $fx, bool $isShort): \Closure { if ($getFxThisFx !== null) {Missing class import via use statement (line '171', column '37'). $getFxThisFxThis = (new \ReflectionFunction($getFxThisFx))->getClosureThis(); if ($getFxThisFxThis !== null) {Missing class import via use statement (line '173', column '27'). throw new \TypeError('New $this getter must be static'); } } $fx = $this->_rebindHookFxToFakeInstance($fx); return static function (self $target, &...$args) use ($getFxThisFx, $fx, $isShort) { if ($getFxThisFx === null) { $fxThis = $target;The method _makeHookDynamicFx uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. } else { $fxThis = $getFxThisFx($target); // @phpstan-ignore argument.type if (!is_object($fxThis)) { // @phpstan-ignore function.alreadyNarrowedTypeMissing class import via use statement (line '185', column '31'). throw new \TypeError('New $this must be an object'); } } return $isShortAvoid using static access to class '\Closure' in method '_makeHookDynamicFx'. ? \Closure::bind($fx, $fxThis)(...$args)Avoid using static access to class '\Closure' in method '_makeHookDynamicFx'. : \Closure::bind($fx, $fxThis)($target, ...$args); }; } /** * Same as onHook() except $this of the callback is dynamically rebound before invoke. * * @param \Closure($this): object $getFxThisFx * @param list<mixed> $args * * @return int<0, max> index under which the hook was added */Method `onHookDynamic` has 5 arguments (exceeds 4 allowed). Consider refactoring.
Avoid variables with short names like $fx. Configured minimum length is 3.
Line exceeds 120 characters; contains 126 characters public function onHookDynamic(string $spot, \Closure $getFxThisFx, \Closure $fx, array $args = [], int $priority = 5): int { return $this->onHook($spot, $this->_makeHookDynamicFx($getFxThisFx, $fx, false), $args, $priority); } /** * Same as onHookDynamic() except no $this is passed to the callback as the 1st argument. * * @param \Closure($this): object $getFxThisFx * @param list<mixed> $args * * @return int<0, max> index under which the hook was added */Method `onHookDynamicShort` has 5 arguments (exceeds 4 allowed). Consider refactoring.
Avoid variables with short names like $fx. Configured minimum length is 3.
Line exceeds 120 characters; contains 131 characters public function onHookDynamicShort(string $spot, \Closure $getFxThisFx, \Closure $fx, array $args = [], int $priority = 5): int { return $this->onHook($spot, $this->_makeHookDynamicFx($getFxThisFx, $fx, true), $args, $priority); } /** * Returns true if at least one callback is defined for this hook. *Line exceeds 120 characters; contains 121 characters * @param ($priorityIsIndex is true ? int<0, max> : int|null) $priority filter specific priority, null for all * @param bool $priorityIsIndex filter by index instead of priority */The method hookHasCallbacks has a boolean flag argument $priorityIsIndex, which is a certain sign of a Single Responsibility Principle violation. public function hookHasCallbacks(string $spot, ?int $priority = null, bool $priorityIsIndex = false): bool { if (!isset($this->hooks[$spot])) { return false; } elseif ($priority === null) { return true; } if ($priorityIsIndex) { $index = $priority; unset($priority); foreach (array_keys($this->hooks[$spot]) as $priority) { if (isset($this->hooks[$spot][$priority][$index])) { return true; } } return false; } Avoid too many `return` statements within this method. return isset($this->hooks[$spot][$priority]); } /** * Delete all hooks for specified spot, priority and index. *Line exceeds 120 characters; contains 121 characters * @param ($priorityIsIndex is true ? int<0, max> : int|null) $priority filter specific priority, null for all * @param bool $priorityIsIndex filter by index instead of priority * * @return static */The method removeHook has a boolean flag argument $priorityIsIndex, which is a certain sign of a Single Responsibility Principle violation. public function removeHook(string $spot, ?int $priority = null, bool $priorityIsIndex = false) { if ($priority !== null) { if ($priorityIsIndex) { $index = $priority; unset($priority); foreach (array_keys($this->hooks[$spot] ?? []) as $priority) { unset($this->hooks[$spot][$priority][$index]); if ($this->hooks[$spot][$priority] === []) { unset($this->hooks[$spot][$priority]); } }The method removeHook uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. } else { unset($this->hooks[$spot][$priority]); } if (($this->hooks[$spot] ?? null) === []) { unset($this->hooks[$spot]); }The method removeHook uses an else expression. Else clauses are basically not necessary and you can simplify the code by not using them. } else { unset($this->hooks[$spot]); } return $this; } /** * Execute all closures assigned to $spot. * * @param list<mixed> $args * @param mixed $brokenBy * * @param-out HookBreaker|null $brokenBy *Line exceeds 120 characters; contains 121 characters * @return array<int<0, max>, mixed>|mixed Array of responses indexed by hook indexes or value specified to breakHook */The method hook() has a Cyclomatic Complexity of 13. The configured cyclomatic complexity threshold is 10. public function hook(string $spot, array $args = [], &$brokenBy = null) { $brokenBy = null; $this->_rebindHooksIfCloned(); $return = []; if (isset($this->hooks[$spot])) { krsort($this->hooks[$spot]); // lower priority is called sooner $hooksBackup = $this->hooks[$spot]; $priorities = array_keys($hooksBackup); try { while (($priority = array_pop($priorities)) !== null) { $hooks2Backup = $this->hooks[$spot][$priority]; $indexes = array_reverse(array_keys($hooks2Backup)); while (($index = array_pop($indexes)) !== null) { [$hookFx, $hookArgs] = $this->hooks[$spot][$priority][$index]; $return[$index] = $hookFx($this, ...$args, ...$hookArgs); if (!isset($this->hooks[$spot][$priority])) { break; } elseif ($hooks2Backup !== $this->hooks[$spot][$priority]) { $hooks2Backup = $this->hooks[$spot][$priority]; $indexes = array_reverse(array_keys($hooks2Backup)); foreach ($indexes as $k => $i) {Avoid deeply nested control flow statements. if ($i <= $index) { unset($indexes[$k]); } } } } if (!isset($this->hooks[$spot])) { // @phpstan-ignore isset.offset break; } elseif ($hooksBackup !== $this->hooks[$spot]) { krsort($this->hooks[$spot]); $hooksBackup = $this->hooks[$spot]; $priorities = array_keys($hooksBackup); foreach ($priorities as $k => $p) { if ($p <= $priority) { unset($priorities[$k]); } } } } } catch (HookBreaker $e) { $brokenBy = $e; return $e->getReturnValue(); } } return $return; } /** * When called from inside a hook closure, it will stop execution of other * closures on the same hook. The passed argument will be returned by the * hook method. * * @param mixed $return What would hook() return? */ public function breakHook($return): void { throw new HookBreaker($return); }}