diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 68fcd9a7..1a783cf3 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -11,6 +11,6 @@ jobs: runs-on: ubuntu-latest steps: # Drafts your next Release notes as Pull Requests are merged into "master" - - uses: toolmantim/release-drafter@master + - uses: toolmantim/release-drafter@v5.6.1 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index cf671a36..3a6a489a 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -15,13 +15,17 @@ jobs: container: image: atk4/image:${{ matrix.php }} # https://github.com/atk4/image strategy: + fail-fast: false matrix: php: ['7.2', '7.3', 'latest'] steps: - - uses: actions/checkout@v1 - # need this to trick composer + - uses: actions/checkout@v2 - run: php --version - - run: "git branch develop; git checkout develop" + + # need this to trick composer that this is a "atk4/core:develop" dependency to install atk4/data + - name: Rename HEAD to develop for Composer + run: git switch -C develop HEAD + - name: Get Composer Cache Directory id: composer-cache run: | diff --git a/docs/Dockerfile b/docs/Dockerfile new file mode 100644 index 00000000..b7ccb758 --- /dev/null +++ b/docs/Dockerfile @@ -0,0 +1,17 @@ +FROM python:2-stretch as builder + +WORKDIR /www + +ADD requirements.txt . + +RUN pip install pip==9.0.1 wheel==0.29.0 \ + && pip install -r requirements.txt + +ADD . . + +RUN make html +#RUN cp -R images build/html/images + +FROM nginx:latest + +COPY --from=builder /www/build/html /usr/share/nginx/html \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index 2e7032e3..ff2243cc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,19 +1,9 @@ -How to build this documentation +Our documentation can now be built in the Docker. ``` -sudo apt-get install python-sphinx python-setuptools -sudo easy_install pip - -sudo pip install sphinx_rtd_theme -sudo pip install sphinxcontrib-phpdomain - -make html +docker build -t docs . +docker run -p 8080:80 docs ``` -next open `html/index.html` in your browser - -``` -open html/index.html -``` - +Open http://localhost:8080/ in your browser. diff --git a/docs/docs.rst b/docs/docs.rst new file mode 100644 index 00000000..c647a41d --- /dev/null +++ b/docs/docs.rst @@ -0,0 +1,43 @@ +================ +Writing ATK Docs +================ + +New users of Agile Toolkit rely on documentation. To make it easier for the +maintainers to update documentation - each component of ATK framework comes +with a nice documentation builder. + +Writing ATK Documentation +========================= + +Open file "docs/index.rst" in your editor. Most editors will support +"reSTructured Text" through add-on. The support is not perfect, but it works. + +If you are updating a feature - find a corresponding ".rst" file. Your editor +may be able to show you a preview. Modify or extend documentation as needed. + +See also: http://www.sphinx-doc.org/en/master/usage/restructuredtext/basics.html + +Building and Testing Documentation +================================== + +Make sure you have "Docker" installed, follow simple instructions in +"docs/README.md". + +Integrating PhpStorm +-------------------- + +You can integrate PhpStorm build process like this: + +.. figure:: images/doc-build-phpstorm1.png + :scale: 50 % + :alt: Create build configuration for the Dockerfile + + +.. figure:: images/doc-build-phpstorm2.png + :scale: 50 % + :alt: Adjust Port settings to expose 80 as 8080 + +.. figure:: images/doc-build-phpstorm3.png + :scale: 50 % + :alt: Use "Ctrl+R" anytime to build docs + diff --git a/docs/factory.rst b/docs/factory.rst index f55a228b..341f733e 100644 --- a/docs/factory.rst +++ b/docs/factory.rst @@ -16,21 +16,33 @@ things like: Thanks to Factory trait, the following code:: - $view->add(['Button', 'A Label', 'icon'=>'book', 'action'=>My\Action::class]); + $button = $app->add(['Button', 'A Label', 'icon'=>'book', 'action'=>My\Action::class]); can replace this:: $button = new \atk4\ui\Button('A Label'); $button->icon = new \atk4\ui\Icon('book'); $button->action = new My\Action(); + $app->add($button); +Type Hinting +------------ + +Agile Toolkit 2.1 introduces support for a new syntax. It is functionally +identical to a short-hand code, but your IDE will properly set type for +a `$button` to be `class Button` instead of `class View`:: + + $button = Button::addTo($view, ['A Label', 'icon'=>'book', 'action'=>My\Action::class]); + +The traditional `$view->add` will remain available, there are no plans to +remove that syntax. Class Name Resolution ===================== Ability to specify only name of the class in ATK is used to keep code clean:: - $app->setLayout('Centered'); + $app->initLayout('Centered'); $form = $app->add('Form'); $form->addField('food_selection', ['Lookup', 'model'=>Model\Food::class]); @@ -40,7 +52,7 @@ various methods are able to accept short identifiers for a simple syntax. .. php:method:: normalizeClassName($name, $prefix = null) -So App::setLayout() would call `normalizeClassName($layout, 'atk4\ui\Layout')`, +So App::initLayout() would call `normalizeClassName($layout, 'atk4\ui\Layout')`, and View::add() would call `normalizeClassName($view, 'atk4\ui')` and finally Form::addField() would call `normalizeClassName($field, 'atk4\ui\FormField` to produce the actual name of the class. @@ -49,11 +61,11 @@ ATK also fully supports the following two alternative forms:: use MyOwn\Layout; - $app->setLayout(Layout::class); + $app->initLayout(Layout::class); and:: - $app->setLayout(new MyOwn\Layout()); + $app->initLayout(new MyOwn\Layout()); In the first case `Layout::class` resolves into string `"MyOwn\Layout"` by the PHP and normalizeClassName would leave it alone. In the former case, object is @@ -69,13 +81,13 @@ classes defined in global namespace:: class TestLayout { } - $app->setLayout(TestLayout::class); - // same as $app->setLayout('TestLayout'); + $app->initLayout(TestLayout::class); + // same as $app->initLayout('TestLayout'); In this case, the call to normalizeClassName would proceed to prefix 'TestLayout' layout into `\atk4\ui\Layout\Test`. -The solution in this case is to use `$app->setLayout('\TestLayout')` +The solution in this case is to use `$app->initLayout('\TestLayout')` Name Resoluion Safety --------------------- @@ -83,17 +95,17 @@ Name Resoluion Safety While you specify name of the layout yourself, it is not a problem, but if you have a code like this:: - $app->setLayout($_GET['layout']); + $app->initLayout($_GET['layout']); Although it seems harmless, technically argument can point to ANY class, which will be loaded and code in this class executed. This can be solved by using a '.' prefix in front of the relative class name:: - $app->setLayout('.'.$_GET['layout']); + $app->initLayout('.'.$_GET['layout']); // or - $app->setLayout('.Centered'); + $app->initLayout('.Centered'); Sub-namespaces -------------- @@ -103,12 +115,12 @@ Sometimes developers prefer to use sub-namespaces, so instead of to use `\atk4\ui\Layout\Centererd\Login`. This becomes another problem for the resolution:: - $app->setLayout('Centered\Login'); + $app->initLayout('Centered\Login'); This actually looks like the a string generated by `use Centered; Login::class` and to avoid, the following syntax can be used:: - $app->setLayout('Centered/Login'); + $app->initLayout('Centered/Login'); Substituting / Overriding classes --------------------------------- diff --git a/docs/hook.rst b/docs/hook.rst index 3719793c..0b3b4be2 100644 --- a/docs/hook.rst +++ b/docs/hook.rst @@ -30,12 +30,12 @@ The framework or application would typically execute hooks like this:: You can register multiple call-backs to be executed for the requested `spot`:: - $obj->addHook('spot', function($obj){ echo "Hook 'spot' is called!"; }); + $obj->onHook('spot', function($obj){ echo "Hook 'spot' is called!"; }); Adding callbacks ================ -.. php:method:: addHook($spot, $callback, $args = null, $priority = 5) +.. php:method:: onHook($spot, $fx = null, array $args = [], int $priority = 5) Register a call-back method. Calling several times will register multiple callbacks which will be execute in the order that they were added. @@ -43,13 +43,15 @@ callbacks which will be execute in the order that they were added. Short way to describe callback method ===================================== -There is a concise syntax for using $callback by specifying object only. +There is a concise syntax for using $fx by specifying object only. +In case $fx is omitted then $this object is used as $fx. + In this case a method with same name as $spot will be used as callback:: function init() { parent::init(); - $this->addHook('beforeUpdate', $this); + $this->onHook('beforeUpdate'); } function beforeUpdate($obj){ @@ -67,20 +69,20 @@ hook with priority 1 it will always be executed before any hooks with priority Normally hooks are executed in the same order as they are added, however if you use negative priority, then hooks will be executed in reverse order:: - $obj->addHook('spot', third, null, -1); + $obj->onHook('spot', third, [], -1); - $obj->addHook('spot', second, null, -5); - $obj->addHook('spot', first, null, -5); + $obj->onHook('spot', second, [], -5); + $obj->onHook('spot', first, [], -5); - $obj->addHook('spot', fourth, null, 0); - $obj->addHook('spot', fifth, null, 0); + $obj->onHook('spot', fourth, [], 0); + $obj->onHook('spot', fifth, [], 0); - $obj->addHook('spot', ten, null, 1000); + $obj->onHook('spot', ten, [], 1000); - $obj->addHook('spot', sixth, null, 2); - $obj->addHook('spot', seventh, null, 5); - $obj->addHook('spot', eight); - $obj->addHook('spot', nine, null, 5); + $obj->onHook('spot', sixth, [], 2); + $obj->onHook('spot', seventh, [], 5); + $obj->onHook('spot', eight); + $obj->onHook('spot', nine, [], 5); .. php:method:: hook($spot, $args = null) @@ -96,8 +98,8 @@ will be placed in array and returned by hook():: return $a+$b; }; - $obj->addHook('test', $mul); - $obj->addHook('test', $add); + $obj->onHook('test', $mul); + $obj->onHook('test', $add); $res1 = $obj->hook('test', [2, 2]); // res1 = [4, 4] @@ -112,7 +114,7 @@ As you see in the code above, we were able to pass some arguments into those hooks. There are actually 3 sources that are considered for the arguments: - first argument to callbacks is always the $object - - arguments passed as 3rd argument to addHook() are included + - arguments passed as 3rd argument to onHook() are included - arguments passed as 2nd argument to hook() are included You can also use key declarations if you wish to override arguments:: @@ -123,8 +125,8 @@ You can also use key declarations if you wish to override arguments:: return pow($a, $power)+$pow($b, $power); } - $obj->addHook('test', $pow, [2]); - $obj->addHook('test', $pow, [7]); + $obj->onHook('test', $pow, [2]); + $obj->onHook('test', $pow, [7]); // execute all 3 hooks $res3 = $obj->hook('test', [2, 2]); @@ -151,13 +153,13 @@ Remember that adding breaking hook with a lower priority can prevent other call-backs from being executed:: - $obj->addHook('test', function($obj){ + $obj->onHook('test', function($obj){ $obj->breakHook("break1"); }); - $obj->addHook('test', function($obj){ + $obj->onHook('test', function($obj){ $obj->breakHook("break2"); - }, null, -5); + }, [], -5); $res3 = $obj->hook('test', [4, 4]); // res3 = "break2" @@ -176,7 +178,7 @@ value is set it may call normalization hook (methods will change $value):: $this->data[$field] = $value; } - $m->addHook('normalize', function(&$a) { $a = trim($a); }); + $m->onHook('normalize', function(&$a) { $a = trim($a); }); Checking if hook has callbacks ============================== diff --git a/docs/images/doc-build-phpstorm1.png b/docs/images/doc-build-phpstorm1.png new file mode 100644 index 00000000..4b730486 Binary files /dev/null and b/docs/images/doc-build-phpstorm1.png differ diff --git a/docs/images/doc-build-phpstorm2.png b/docs/images/doc-build-phpstorm2.png new file mode 100644 index 00000000..a056c176 Binary files /dev/null and b/docs/images/doc-build-phpstorm2.png differ diff --git a/docs/images/doc-build-phpstorm3.png b/docs/images/doc-build-phpstorm3.png new file mode 100644 index 00000000..ec885f4f Binary files /dev/null and b/docs/images/doc-build-phpstorm3.png differ diff --git a/docs/index.rst b/docs/index.rst index e073b916..55def48a 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -20,7 +20,7 @@ Object Containers .. figure:: images/containers.png :scale: 50 % -Within your application / framework you can quite often have requirement for +Within your application or framework you can quite often have requirement for using containers: - Form containing fields @@ -134,8 +134,8 @@ and triggering callbacks:: $object = new AnyClass(); - $object->addHook('test', function($o){ echo 'hello'; } - $object->addHook('test', function($o){ echo 'world'; } + $object->onHook('test', function($o){ echo 'hello'; } + $object->onHook('test', function($o){ echo 'world'; } $object->hook('test'); // outputs: helloworld @@ -273,3 +273,4 @@ Others debug session + docs diff --git a/docs/requirements.txt b/docs/requirements.txt index 3f092f37..e3037e09 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,2 @@ sphinxcontrib-phpdomain +sphinx_rtd_theme diff --git a/examples/test2.php b/examples/test2.php index 4b824019..0cd2c645 100644 --- a/examples/test2.php +++ b/examples/test2.php @@ -17,7 +17,7 @@ public function doWork() } $c = new MyClass(); -$c->addHook('afterWork', function () { +$c->onHook('afterWork', function () { echo "HOOKed on work\n"; }); $c->doWork(); diff --git a/src/DynamicMethodTrait.php b/src/DynamicMethodTrait.php index ecc67edb..5a8bf22c 100644 --- a/src/DynamicMethodTrait.php +++ b/src/DynamicMethodTrait.php @@ -19,40 +19,40 @@ trait DynamicMethodTrait * Magic method - tries to call dynamic method and throws exception if * this was not possible. * - * @param string $name Name of the method - * @param array $arguments Array of arguments to pass to this method + * @param string $name Name of the method + * @param array $args Array of arguments to pass to this method */ - public function __call(string $method, $arguments) + public function __call(string $method, $args) { - if ($ret = $this->tryCall($method, $arguments)) { - return $ret[0]; + if ($ret = $this->tryCall($method, $args)) { + return reset($ret); } throw new Exception([ 'Method '.$method.' is not defined for this object', 'class' => get_class($this), 'method' => $method, - 'arguments' => $arguments, + 'args' => $args, ]); } /** * Tries to call dynamic method. * - * @param string $name Name of the method - * @param array $arguments Array of arguments to pass to this method + * @param string $name Name of the method + * @param array $args Array of arguments to pass to this method * * @return mixed|null */ - public function tryCall($method, $arguments) + public function tryCall($method, $args) { - if (isset($this->_hookTrait) && $ret = $this->hook('method-'.$method, $arguments)) { + if (isset($this->_hookTrait) && $ret = $this->hook('method-'.$method, $args)) { return $ret; } if (isset($this->_appScopeTrait) && isset($this->app->_hookTrait)) { - array_unshift($arguments, $this); - if ($ret = $this->app->hook('global-method-'.$method, $arguments)) { + array_unshift($args, $this); + if ($ret = $this->app->hook('global-method-'.$method, $args)) { return $ret; } } @@ -61,12 +61,12 @@ public function tryCall($method, $arguments) /** * Add new method for this object. * - * @param string|array $name Name of new method of $this object - * @param callable $callable Callback + * @param string|array $name Name of new method of $this object + * @param callable $fx Callback * * @return $this */ - public function addMethod($name, $callable) + public function addMethod($name, $fx) { // HookTrait is mandatory if (!isset($this->_hookTrait)) { @@ -79,21 +79,21 @@ public function addMethod($name, $callable) if (is_array($name)) { foreach ($name as $h) { - $this->addMethod($h, $callable); + $this->addMethod($h, $fx); } return $this; } - if (is_object($callable) && !is_callable($callable)) { - $callable = [$callable, $name]; + if (is_object($fx) && !is_callable($fx)) { + $fx = [$fx, $name]; } if ($this->hasMethod($name)) { throw new Exception(['Registering method twice', 'name' => $name]); } - $this->addHook('method-'.$name, $callable); + $this->onHook('method-'.$name, $fx); return $this; } @@ -146,10 +146,10 @@ public function removeMethod($name) * @see self::hasMethod() * @see self::__call() * - * @param string $name Name of the method - * @param callable $callable Calls your function($object, $arg1, $arg2) + * @param string $name Name of the method + * @param callable $fx Calls your function($object, $arg1, $arg2) */ - public function addGlobalMethod($name, $callable) + public function addGlobalMethod($name, $fx) { // AppScopeTrait and HookTrait for app are mandatory if (!isset($this->_appScopeTrait) || !isset($this->app->_hookTrait)) { @@ -159,7 +159,7 @@ public function addGlobalMethod($name, $callable) if ($this->hasGlobalMethod($name)) { throw new Exception(['Registering global method twice', 'name' => $name]); } - $this->app->addHook('global-method-'.$name, $callable); + $this->app->onHook('global-method-'.$name, $fx); } /** diff --git a/src/Exception.php b/src/Exception.php index d56459fe..35888bc5 100644 --- a/src/Exception.php +++ b/src/Exception.php @@ -13,7 +13,6 @@ use atk4\core\ExceptionRenderer\RendererAbstract; use atk4\core\Translator\ITranslatorAdapter; use atk4\core\Translator\Translator; -use Throwable; /** * All exceptions generated by Agile Core will use this class. @@ -51,17 +50,23 @@ class Exception extends \Exception * * @param string|array $message * @param int $code - * @param Throwable $previous + * @param \Throwable $previous */ public function __construct( $message = '', ?int $code = null, - Throwable $previous = null + \Throwable $previous = null ) { if (is_array($message)) { // message contain additional parameters $this->params = $message; $message = array_shift($this->params); + if (isset($this->params['solutions'])) { + foreach ((array) $this->params['solutions'] as $solution) { + $this->addSolution($solution); + } + unset($this->params['solutions']); + } } parent::__construct($message, $code ?? 0, $previous); diff --git a/src/ExceptionRenderer/Console.php b/src/ExceptionRenderer/Console.php index e966d640..686e334d 100644 --- a/src/ExceptionRenderer/Console.php +++ b/src/ExceptionRenderer/Console.php @@ -26,7 +26,7 @@ protected function processHeader(): void \e[1;41m--[ {TITLE} ]\e[0m {CLASS}: \e[1;30m{MESSAGE}\e[0;31m {CODE} TEXT -); + ); } protected function processParams(): void diff --git a/src/ExceptionRenderer/HTML.php b/src/ExceptionRenderer/HTML.php index eea9e2a3..e35fecf5 100644 --- a/src/ExceptionRenderer/HTML.php +++ b/src/ExceptionRenderer/HTML.php @@ -109,7 +109,7 @@ protected function processStackTrace(): void $this->output .= ' - + '; @@ -123,7 +123,7 @@ protected function processStackTrace(): void protected function processStackTraceInternal(): void { - $text = ' + $text = ' diff --git a/src/ExceptionRenderer/HTMLText.php b/src/ExceptionRenderer/HTMLText.php index 07276c83..d8ca26fa 100644 --- a/src/ExceptionRenderer/HTMLText.php +++ b/src/ExceptionRenderer/HTMLText.php @@ -27,7 +27,7 @@ protected function processHeader(): void {CLASS}: {MESSAGE} {CODE} HTML -); + ); } protected function processParams(): void diff --git a/src/HookTrait.php b/src/HookTrait.php index ebd69b06..f594f7dc 100644 --- a/src/HookTrait.php +++ b/src/HookTrait.php @@ -18,93 +18,106 @@ trait HookTrait */ protected $hooks = []; + /** + * Next hook index counter. + * + * @var int + */ + private $_hookIndexCounter = 0; + + /** + * @deprecated use onHook instead + */ + public function addHook($spot, $fx, array $args = null, int $priority = null) + { + return $this->onHook($spot, $fx, $args ?? [], $priority ?? 0); + } + /** * Add another callback to be executed during hook($hook_spot);. * * If priority is negative, then hooks will be executed in reverse order. * - * @param string $hook_spot Hook identifier to bind on - * @param object|callable $callable Will be called on hook() - * @param array $arguments Arguments are passed to $callable - * @param int $priority Lower priority is called sooner + * @param string|string[] $spot Hook identifier to bind on + * @param object|callable|null $fx Will be called on hook() + * @param array $args Arguments are passed to $fx + * @param int $priority Lower priority is called sooner * - * @return $this + * @return int|int[] Index under which the hook was added */ - public function addHook($hook_spot, $callable, $arguments = null, $priority = null) + public function onHook($spot, $fx = null, array $args = [], int $priority = 5) { - - // Set defaults - if (is_null($arguments)) { - $arguments = []; - } elseif (!is_array($arguments)) { - throw new Exception(['Incorrect arguments for addHook', 'args' => $arguments]); - } - if (is_null($priority)) { - $priority = 5; - } + $fx = $fx ?: $this; // multiple hooks can be linked - if (is_string($hook_spot) && strpos($hook_spot, ',') !== false) { - $hook_spot = explode(',', $hook_spot); + if (is_string($spot) && strpos($spot, ',') !== false) { + $spot = explode(',', $spot); } - if (is_array($hook_spot)) { - foreach ($hook_spot as $h) { - $this->addHook($h, $callable, $arguments, $priority); + if (is_array($spot)) { + $indexes = []; + foreach ($spot as $k => $h) { + $indexes[$k] = $this->onHook($h, $fx, $args, $priority); } - return $this; + return $indexes; } + $spot = (string) $spot; - // short for addHook('test', $this); to call $this->test(); - if (!is_callable($callable)) { - if (is_object($callable)) { - if (isset($callable->_dynamicMethodTrait)) { - if (!$callable->hasMethod($hook_spot)) { - throw new Exception([ - '$callable should be a valid callback', - 'callable' => $callable, - ]); - } - } else { - if (!method_exists($callable, $hook_spot)) { - throw new Exception([ - '$callable should be a valid callback', - 'callable' => $callable, - ]); - } - } - $callable = [$callable, $hook_spot]; - } else { + // short for onHook('test', $this); to call $this->test(); + if (!is_callable($fx)) { + $valid = false; + if (is_object($fx)) { + $valid = (isset($fx->_dynamicMethodTrait) && $fx->hasMethod($spot)) || method_exists($fx, $spot); + } + + if (!$valid) { throw new Exception([ - '$callable should be a valid callback', - 'callable' => $callable, + '$fx should be a valid callback', + 'fx' => $fx, ]); } + + $fx = [$fx, $spot]; } - if (!isset($this->hooks[$hook_spot][$priority])) { - $this->hooks[$hook_spot][$priority] = []; + if (!isset($this->hooks[$spot][$priority])) { + $this->hooks[$spot][$priority] = []; } - if ($priority >= 0) { - $this->hooks[$hook_spot][$priority][] = [$callable, $arguments]; + $index = $this->_hookIndexCounter++; + $data = [$fx, $args]; + if ($priority < 0) { + $this->hooks[$spot][$priority] = [$index => $data] + $this->hooks[$spot][$priority]; } else { - array_unshift($this->hooks[$hook_spot][$priority], [$callable, $arguments]); + $this->hooks[$spot][$priority][$index] = $data; } - return $this; + return $index; } /** - * Delete all hooks for specified spot. + * Delete all hooks for specified spot, priority and index. * - * @param string $hook_spot Hook identifier to bind on + * @param string $spot Hook identifier + * @param int|null $priority Filter specific priority, null for all + * @param int|null $priorityIsIndex Filter by index instead of priority * - * @return $this + * @return static */ - public function removeHook($hook_spot) + public function removeHook(string $spot, int $priority = null, bool $priorityIsIndex = false) { - unset($this->hooks[$hook_spot]); + if ($priority !== null) { + if ($priorityIsIndex) { + $index = $priority; + foreach (array_keys($this->hooks[$spot]) as $priority) { + unset($this->hooks[$spot][$priority][$index]); + } + } else { + unset($this->hooks[$spot][$priority]); + } + } else { + unset($this->hooks[$spot]); + } return $this; } @@ -112,64 +125,71 @@ public function removeHook($hook_spot) /** * Returns true if at least one callback is defined for this hook. * - * @param string $hook_spot Hook identifier - * - * @return bool + * @param string $spot Hook identifier + * @param int|null $priority Filter specific priority, null for all + * @param int|null $priorityIsIndex Filter by index instead of priority */ - public function hookHasCallbacks($hook_spot) + public function hookHasCallbacks(string $spot, int $priority = null, bool $priorityIsIndex = false): bool { - return isset($this->hooks[$hook_spot]); + if (!isset($this->hooks[$spot])) { + return false; + } elseif ($priority === null) { + return true; + } + + if ($priorityIsIndex) { + $index = $priority; + foreach (array_keys($this->hooks[$spot]) as $priority) { + if (isset($this->hooks[$spot][$priority][$index])) { + return true; + } + } + + return false; + } + + return isset($this->hooks[$spot][$priority]); } /** * Execute all callables assigned to $hook_spot. * - * @param string $hook_spot Hook identifier - * @param array $arg Additional arguments to callables + * @param string $spot Hook identifier + * @param array $args Additional arguments to callables * * @throws Exception * - * @return mixed Array of responses or value specified to breakHook + * @return mixed Array of responses indexed by hook indexes or value specified to breakHook */ - public function hook($hook_spot, $arg = null) + public function hook(string $spot, array $args = []) { - if (is_null($arg)) { - $arg = []; - } elseif (!is_array($arg)) { - throw new Exception([ - 'Arguments for callbacks should be passed as array', - 'arg' => $arg, - ]); - } - $return = []; - try { - if ( - isset($this->hooks[$hook_spot]) - && is_array($this->hooks[$hook_spot]) - ) { - krsort($this->hooks[$hook_spot]); // lower priority is called sooner - $hook_backup = $this->hooks[$hook_spot]; - while ($_data = array_pop($this->hooks[$hook_spot])) { - foreach ($_data as &$data) { - $return[] = call_user_func_array( + if (isset($this->hooks[$spot])) { + krsort($this->hooks[$spot]); // lower priority is called sooner + $hookBackup = $this->hooks[$spot]; + + try { + while ($_data = array_pop($this->hooks[$spot])) { + foreach ($_data as $index => &$data) { + $return[$index] = call_user_func_array( $data[0], array_merge( [$this], - $arg, + $args, $data[1] ) ); } } + unset($data); - $this->hooks[$hook_spot] = $hook_backup; - } - } catch (HookBreaker $e) { - $this->hooks[$hook_spot] = $hook_backup; + $this->hooks[$spot] = $hookBackup; + } catch (HookBreaker $e) { + $this->hooks[$spot] = $hookBackup; - return $e->return_value; + return $e->return_value; + } } return $return; diff --git a/src/StaticAddToTrait.php b/src/StaticAddToTrait.php new file mode 100644 index 00000000..001ab0b5 --- /dev/null +++ b/src/StaticAddToTrait.php @@ -0,0 +1,130 @@ + ['name']]); + * is equivalent to + * $crud = $app->add(['CRUD', 'displayFields' => ['name']]); + * but the first one design pattern is strongly recommended as it supports refactoring. + * + * @param array|string $seed + * + * @return static + */ + public static function addTo(object $parent, $seed = [], array $add_args = [], bool $skip_add = false) + { + if (is_object($seed)) { + $object = $seed; + } else { + if (!is_array($seed)) { + if (!is_scalar($seed)) { // allow single element seed but prevent bad usage + throw (new Exception('Seed must be an array or a scalar')) + ->addMoreInfo('seed_type', gettype($seed)); + } + + $seed = [$seed]; + } + + if (isset($parent->_factoryTrait)) { + $object = $parent->factory(static::class, $seed); + } else { + $object = new static(...$seed); + } + } + + return static::_addTo_add($parent, $object, false, $add_args, $skip_add); + } + + /** + * @return static + */ + private static function _addTo_add(object $parent, object $object, bool $unsafe, array $add_args, bool $skip_add = false) + { + // check if object is instance of this class + if (!$unsafe && !($object instanceof static)) { + throw (new Exception('Seed class name is not a subtype of the current class')) + ->addMoreInfo('seed_class', get_class($object)) + ->addMoreInfo('current_class', static::class); + } + + // add to parent + if (!$skip_add) { + $parent->add($object, ...$add_args); + } + + return $object; + } + + /** + * Same as addTo(), but the first element of seed specifies a class name instead of static::class. + * + * @param array|string $seed The first element specifies a class name, other element are seed + * + * @return static + */ + public static function addToWithClassName(object $parent, $seed = [], array $add_args = [], bool $skip_add = false) + { + return static::_addToWithClassName($parent, $seed, false, $add_args, $skip_add); + } + + /** + * Same as addToWithClassName(), but the new object is not checked if it is instance of this class. + * + * @param array|string $seed The first element specifies a class name, other element are seed + * + * @return static + */ + public static function addToWithClassNameUnsafe(object $parent, $seed = [], array $add_args = [], bool $skip_add = false) + { + return static::_addToWithClassName($parent, $seed, true, $add_args, $skip_add); + } + + /** + * @return static + */ + private static function _addToWithClassName(object $parent, $seed, bool $unsafe, array $add_args, bool $skip_add = false) + { + if (is_object($seed)) { + $object = $seed; + } else { + if (!is_array($seed)) { + if (!is_scalar($seed)) { // allow single element seed but prevent bad usage + throw (new Exception('Seed must be an array or a scalar')) + ->addMoreInfo('seed_type', gettype($seed)); + } + + $seed = [$seed]; + } + + if (!isset($seed[0])) { + throw new Exception('Class name in seed is not defined'); + } + + if (isset($parent->_factoryTrait)) { + $object = $parent->factory($seed); + } else { + $cl = $seed[0]; + unset($seed[0]); + $object = new $cl(...$seed); + } + } + + return static::_addTo_add($parent, $object, $unsafe, $add_args, $skip_add); + } +} diff --git a/tests/CollectionMock.php b/tests/CollectionMock.php index 15900e0a..b47553b9 100644 --- a/tests/CollectionMock.php +++ b/tests/CollectionMock.php @@ -12,9 +12,6 @@ class CollectionMock protected $fields = []; /** - * @param $name - * @param $seed - * * @throws core\Exception * * @return mixed|object @@ -34,8 +31,6 @@ public function hasField($name) } /** - * @param $name - * * @throws core\Exception * * @return mixed diff --git a/tests/ExceptionTest.php b/tests/ExceptionTest.php index 24d057f9..4b289240 100644 --- a/tests/ExceptionTest.php +++ b/tests/ExceptionTest.php @@ -7,7 +7,6 @@ use atk4\core\Exception; use atk4\core\TrackableTrait; use PHPUnit\Framework\TestCase; -use StdClass; /** * @coversDefaultClass \atk4\core\Exception @@ -63,7 +62,7 @@ public function testColorfulText(): void $ret = $m->toString('abc'); $this->assertEquals('"abc"', $ret); - $ret = $m->toString(new StdClass()); + $ret = $m->toString(new \stdClass()); $this->assertEquals('Object stdClass', $ret); $a = new TrackableMock2(); @@ -117,6 +116,29 @@ public function testSolution(): void $this->assertRegExp('/One Solution/', $ret); } + public function testSolution2(): void + { + $m = new Exception([ + 'Exception with solution', + 'solutions' => '1st Solution', + ]); + + $ret = $m->getColorfulText(); + $this->assertRegExp('/1st Solution/', $ret); + + $m = new Exception([ + 'Exception with solution', + 'solutions' => [ + '1st Solution', + '2nd Solution', + ], + ]); + + $ret = $m->getColorfulText(); + $this->assertRegExp('/1st Solution/', $ret); + $this->assertRegExp('/2nd Solution/', $ret); + } + public function testCustomName(): void { $m = new ExceptionCustomName( diff --git a/tests/HookTraitTest.php b/tests/HookTraitTest.php index 2e63c3b3..5c257dff 100644 --- a/tests/HookTraitTest.php +++ b/tests/HookTraitTest.php @@ -11,13 +11,22 @@ */ class HookTraitTest extends TestCase { - public function testException1() + public function testArguments() { - $this->expectException(Exception::class); $m = new HookMock(); - $m->addHook('test1', function () use (&$result) { + + $result = 0; + $m->onHook('test1', function () use (&$result) { + $result++; + }, [0]); + + $this->assertEquals(0, $result); + + $m->onHook('test1', function () use (&$result) { $result++; - }, 'incorrect_argument'); + }, [5]); + + $this->assertEquals(0, $result); } /** @@ -28,7 +37,7 @@ public function testBasic() $m = new HookMock(); $result = 0; - $m->addHook('test1', function () use (&$result) { + $m->onHook('test1', function () use (&$result) { $result++; }); @@ -44,13 +53,13 @@ public function testAdvanced() $m = new HookMock(); $result = 20; - $m->addHook('test1', function () use (&$result) { + $m->onHook('test1', function () use (&$result) { $result++; }); - $m->addHook('test1', function () use (&$result) { + $m->onHook('test1', function () use (&$result) { $result = 0; - }, null, 1); + }, [], 1); $m->hook('test1'); // zero will be executed first, then increment $this->assertEquals(1, $result); @@ -61,7 +70,7 @@ public function testMultiple() $m = new HookMock(); $result = 0; - $m->addHook(['test1,test2', 'test3'], function () use (&$result) { + $m->onHook(['test1,test2', 'test3'], function () use (&$result) { $result++; }); @@ -96,7 +105,7 @@ public function testCallable() $m = new HookMock(); $this->result = 0; - $m->addHook('tst', $this); + $m->onHook('tst', $this); $m->hook('tst'); $this->assertEquals(1, $this->result); @@ -106,7 +115,7 @@ public function testCallable() // Existing method - foo $m = new HookWithDynamicMethodMock(); - $m->addHook('foo', $m); + $m->onHook('foo', $m); } public function testCallableException1() @@ -114,7 +123,7 @@ public function testCallableException1() // unknown method $this->expectException(Exception::class); $m = new HookMock(); - $m->addHook('unknown_method', $m); + $m->onHook('unknown_method', $m); } public function testCallableException2() @@ -122,7 +131,7 @@ public function testCallableException2() // not existing dynamic method $this->expectException(Exception::class); $m = new HookWithDynamicMethodMock(); - $m->addHook('unknown_method', $m); + $m->onHook('unknown_method', $m); } public function testCallableException3() @@ -130,58 +139,75 @@ public function testCallableException3() // wrong 2nd argument $this->expectException(Exception::class); $m = new HookMock(); - $m->addHook('unknown_method', 'incorrect_param'); + $m->onHook('unknown_method', 'incorrect_param'); } public function testHookException1() { // wrong 2nd argument - $this->expectException(Exception::class); $m = new HookMock(); - $m->addHook('tst', $this); - $m->hook('tst', 'wrong_parameter'); + + $result = ''; + $m->onHook('tst', function ($m, $arg) use (&$result) { + $result .= $arg; + }); + + $m->hook('tst', ['parameter']); + + $this->assertEquals('parameter', $result); } public function testOrder() { $m = new HookMock(); - $m->addHook('spot', function () { + $ind = $m->onHook('spot', function () { return 3; - }, null, -1); - $m->addHook('spot', function () { + }, [], -1); + $m->onHook('spot', function () { return 2; - }, null, -5); - $m->addHook('spot', function () { + }, [], -5); + $m->onHook('spot', function () { return 1; - }, null, -5); + }, [], -5); - $m->addHook('spot', function () { + $m->onHook('spot', function () { return 4; - }, null, 0); - $m->addHook('spot', function () { + }, [], 0); + $m->onHook('spot', function () { return 5; - }, null, 0); + }, [], 0); - $m->addHook('spot', function () { + $m->onHook('spot', function () { return 10; - }, null, 1000); + }, [], 1000); - $m->addHook('spot', function () { + $m->onHook('spot', function () { return 6; - }, null, 2); - $m->addHook('spot', function () { + }, [], 2); + $m->onHook('spot', function () { return 7; - }, null, 5); - $m->addHook('spot', function () { + }, [], 5); + $m->onHook('spot', function () { return 8; }); - $m->addHook('spot', function () { + $m->onHook('spot', function () { return 9; - }, null, 5); + }, [], 5); $ret = $m->hook('spot'); - $this->assertEquals([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], $ret); + $this->assertEquals([ + $ind + 2 => 1, + $ind + 1 => 2, + $ind => 3, + $ind + 3 => 4, + $ind + 4 => 5, + $ind + 6 => 6, + $ind + 7 => 7, + $ind + 8 => 8, + $ind + 9 => 9, + $ind + 5 => 10, + ], $ret); } public function testMulti() @@ -196,8 +222,8 @@ public function testMulti() return $a + $b; }; - $obj->addHook('test', $mul); - $obj->addHook('test', $add); + $obj->onHook('test', $mul); + $obj->onHook('test', $add); $res1 = $obj->hook('test', [2, 2]); $this->assertEquals([4, 4], $res1); @@ -222,10 +248,10 @@ public function testArgs() return pow($a, $power) + pow($b, $power); }; - $obj->addHook('test', $mul); - $obj->addHook('test', $add); - $obj->addHook('test', $pow, [2]); - $obj->addHook('test', $pow, [7]); + $obj->onHook('test', $mul); + $obj->onHook('test', $add); + $obj->onHook('test', $pow, [2]); + $obj->onHook('test', $pow, [7]); $res1 = $obj->hook('test', [2, 2]); $this->assertEquals([4, 4, 8, 256], $res1); @@ -242,7 +268,7 @@ public function testReferences() $a++; }; - $obj->addHook('inc', $inc); + $obj->onHook('inc', $inc); $v = 1; $a = [&$v]; $obj->hook('inc', $a); @@ -256,7 +282,7 @@ public function testReferences() }; $v = 1; - $obj->addHook('inc', $inc); + $obj->onHook('inc', $inc); $obj->hook('inc', [&$v]); $this->assertEquals(2, $v); @@ -265,7 +291,7 @@ public function testReferences() public function testDefaultMethod() { $obj = new HookMock(); - $obj->addHook('myCallback', $obj); + $obj->onHook('myCallback', $obj); $obj->hook('myCallback'); $this->assertEquals(1, $obj->result); @@ -283,9 +309,9 @@ public function testBreakHook() } }; - $m->addHook('inc', $inc); - $m->addHook('inc', $inc); - $m->addHook('inc', $inc); + $m->onHook('inc', $inc); + $m->onHook('inc', $inc); + $m->onHook('inc', $inc); $ret = $m->hook('inc'); $this->assertEquals(2, $m->result); @@ -302,7 +328,7 @@ public function testExceptionInHook() throw new \atk4\core\Exception(['stuff went wrong']); }; - $m->addHook('inc', $inc); + $m->onHook('inc', $inc); $ret = $m->hook('inc'); } } diff --git a/tests/LocalizationTest.php b/tests/LocalizationTest.php index 4229a9d8..c7f3a718 100644 --- a/tests/LocalizationTest.php +++ b/tests/LocalizationTest.php @@ -55,11 +55,12 @@ public function testTranslatableExternal() /* @var $e Exception */ // emulate an external translator already configured $e->setTranslatorAdapter(new class() implements ITranslatorAdapter { - public function _(string $message, + public function _( + string $message, array $parameters = [], ?string $domain = null, - ?string $locale = null): string - { + ?string $locale = null + ): string { return 'external translator'; } }); diff --git a/tests/SeedTest.php b/tests/SeedTest.php index 7cdad9f7..de8ae4fc 100644 --- a/tests/SeedTest.php +++ b/tests/SeedTest.php @@ -418,7 +418,8 @@ public function testPropertyMerging() { $s1 = $this->factory( ['atk4/core/tests/SeedDITestMock', 'foo'=>['Button', 'icon'=>'red']], - ['foo'=> ['Label', 'red']]); + ['foo'=> ['Label', 'red']] + ); $this->assertEquals(['Button', 'icon'=>'red'], $s1->foo); diff --git a/tests/StaticAddToTest.php b/tests/StaticAddToTest.php new file mode 100644 index 00000000..d6907b41 --- /dev/null +++ b/tests/StaticAddToTest.php @@ -0,0 +1,150 @@ +c = $name; + } +} +// @codingStandardsIgnoreEnd + +/** + * @coversDefaultClass \atk4\core\StaticAddToTrait + */ +class StaticAddToTest extends TestCase +{ + public function testBasic() + { + $m = new ContainerMock(); + $this->assertEquals(true, isset($m->_containerTrait)); + + // add to return object + $tr = StdSAT::addTo($m); + $this->assertNotNull($tr); + + // add object - for BC + $tr = StdSAT::addTo($m, $tr); + $this->assertEquals(StdSAT::class, get_class($tr)); + + // trackable object can be referenced by name + $tr3 = TrackableMockSAT::addTo($m, [], ['foo']); + $tr = $m->getElement('foo'); + $this->assertEquals($tr, $tr3); + + // not the same or extended class + $this->expectException(\atk4\core\Exception::class); + $tr = StdSAT::addTo($m, $tr); + } + + public function testWithClassName() + { + $m = new ContainerMock(); + $this->assertEquals(true, isset($m->_containerTrait)); + + // the same class + $tr = StdSAT::addToWithClassName($m, StdSAT::class); + $this->assertEquals(StdSAT::class, get_class($tr)); + + // add object - for BC + $tr = StdSAT::addToWithClassName($m, $tr); + $this->assertEquals(StdSAT::class, get_class($tr)); + + // extended class + $tr = StdSAT::addToWithClassName($m, StdSAT2::class); + $this->assertEquals(StdSAT2::class, get_class($tr)); + + // not the same or extended class - unsafe disabled + $this->expectException(\atk4\core\Exception::class); + $tr = StdSAT::addToWithClassName($m, \stdClass::class); + + // not the same or extended class - unsafe enabled + $tr = StdSAT::addToWithClassNameUnsafe($m, \stdClass::class); + $this->assertEquals(\stdClass::class, get_class($tr)); + } + + public function testUniqueNames() + { + $m = new ContainerMock(); + + // two anonymous children should get unique names asigned. + TrackableMockSAT::addTo($m); + $anon = TrackableMockSAT::addTo($m); + TrackableMockSAT::addTo($m, [], ['foo bar']); + TrackableMockSAT::addTo($m, [], ['123']); + TrackableMockSAT::addTo($m, [], ['false']); + + $this->assertEquals(true, (bool) $m->hasElement('foo bar')); + $this->assertEquals(true, (bool) $m->hasElement('123')); + $this->assertEquals(true, (bool) $m->hasElement('false')); + $this->assertEquals(5, $m->getElementCount()); + + $m->getElement('foo bar')->destroy(); + $this->assertEquals(4, $m->getElementCount()); + $anon->destroy(); + $this->assertEquals(3, $m->getElementCount()); + } + + public function testFactoryMock() + { + $m = new ContainerFactoryMockSAT(); + $m1 = DIMockSAT::addTo($m, ['a'=>'XXX', 'b'=>'YYY']); + $this->assertEquals('XXX', $m1->a); + $this->assertEquals('YYY', $m1->b); + $this->assertEquals(null, $m1->c); + + $m2 = DIConstructorMockSAT::addTo($m, ['a'=>'XXX', 'John', 'b'=>'YYY']); + $this->assertEquals('XXX', $m2->a); + $this->assertEquals('YYY', $m2->b); + $this->assertEquals('John', $m2->c); + } +} diff --git a/tests/config_test/config.json b/tests/config_test/config.json index 574143d2..f90e2a0a 100644 --- a/tests/config_test/config.json +++ b/tests/config_test/config.json @@ -1,14 +1,14 @@ -{ - "num": 123, - "txt": "foo", - "bool": true, - "obj": { - "num": 456, - "txt": "bar", - "bool": true - }, - "arr": [ - {"one": "one", "another": "another"}, - {"two": "two"} - ] -} +{ + "num": 123, + "txt": "foo", + "bool": true, + "obj": { + "num": 456, + "txt": "bar", + "bool": true + }, + "arr": [ + {"one": "one", "another": "another"}, + {"two": "two"} + ] +} diff --git a/tests/config_test/config.yml b/tests/config_test/config.yml index 67774b7b..1af4ac18 100644 --- a/tests/config_test/config.yml +++ b/tests/config_test/config.yml @@ -1,11 +1,11 @@ -num: 123 -txt: foo -bool: true -obj: - num: 456 - txt: bar - bool: true -arr: -- one: one - another: another -- two: two +num: 123 +txt: foo +bool: true +obj: + num: 456 + txt: bar + bool: true +arr: +- one: one + another: another +- two: two
Stack Trace
#FileObjectMethod
#FileObjectMethod
{INDEX} {FILE_LINE}