From 34a6da6dd60f9cff4d58f9299d1aad24807878d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20Vo=C5=99=C3=AD=C5=A1ek?= Date: Wed, 2 Dec 2020 15:13:44 +0100 Subject: [PATCH] Add PHPStan to CI (#1549) * Add PHPStan to CI * fix memory limit * level = 0 * do not rely solely on phpdoc * exclude new static rule * fix undefined var * fix missing use * fix method signature * phpstan can not analyse included variables * level = 1 * fix some issues * fix phpstan config escape * fix return mixed type with null * skip ignore for level=2 * fix model->expr() * add generated ignores * increase level to 2 * fix unneeded ignore * fix wrong phpdoc * fix duplicate keys in ScopeBuilder * unify value of OPERATOR_TEXT_EQUALS --- .github/workflows/test-unit.yml | 15 +- composer.json | 1 + demos/_includes/Persistence_Faker.php | 10 +- demos/_unit-test/exception.php | 4 +- demos/_unit-test/lookup.php | 2 +- demos/_unit-test/scope-builder.php | 8 +- demos/form/form.php | 2 +- demos/form/jscondform.php | 6 +- demos/init-app.php | 1 + demos/init-db.php | 2 +- demos/interactive/accordion-nested.php | 4 +- demos/interactive/popup.php | 2 +- docs/form.rst | 4 +- phpstan.neon.dist | 330 +++++++++++++++++++++++++ src/Callback.php | 2 +- src/CallbackLater.php | 2 +- src/CardDeck.php | 2 +- src/Crud.php | 2 +- src/Form.php | 2 +- src/Form/Control/Lookup.php | 7 +- src/Form/Control/ScopeBuilder.php | 4 +- src/Grid.php | 2 +- src/UserAction/ModalExecutor.php | 2 +- tests/DemosTest.php | 2 + tests/FormTest.php | 2 +- tools/get-assets.php | 2 +- 26 files changed, 381 insertions(+), 41 deletions(-) create mode 100644 phpstan.neon.dist diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index 6afce0459e..45b26b1f7b 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -20,6 +20,8 @@ jobs: include: - php: 'latest' type: 'CodingStyle' + - php: 'latest' + type: 'StaticAnalysis' env: LOG_COVERAGE: "" steps: @@ -46,9 +48,9 @@ jobs: - name: Install PHP dependencies run: | - if [ "${{ matrix.type }}" != "Phpunit" ]; then composer remove --no-interaction --no-update phpunit/phpunit johnkary/phpunit-speedtrap phpunit/phpcov --dev ; fi + if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpunit/phpunit johnkary/phpunit-speedtrap phpunit/phpcov --dev ; fi if [ "${{ matrix.type }}" != "CodingStyle" ]; then composer remove --no-interaction --no-update friendsofphp/php-cs-fixer --dev ; fi - composer remove --no-interaction --no-update 'behat/*' --dev + if [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpstan/phpstan 'behat/*' --dev ; fi if [ "${{ matrix.php }}" == "8.0" ]; then composer config platform.php 7.4.5 ; fi composer install --no-suggest --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader @@ -79,6 +81,12 @@ jobs: if [ "$(find demos/ -name '*.php' -print0 | xargs -0 grep -L "namespace atk4\\\\ui\\\\demo;" | tee /dev/fd/2)" ]; then echo 'All demos/ files must have namespace declared' && (exit 1); fi vendor/bin/php-cs-fixer fix --dry-run --using-cache=no --diff --diff-format=udiff --verbose --show-progress=dots + - name: Run Static Analysis (only for StaticAnalysis) + if: matrix.type == 'StaticAnalysis' + run: | + echo "memory_limit = 1G" > /usr/local/etc/php/conf.d/custom-memory-limit.ini + vendor/bin/phpstan analyse + unit-test: name: Unit runs-on: ubuntu-latest @@ -132,7 +140,7 @@ jobs: run: | if [ "${{ matrix.type }}" != "Phpunit" ] && [ "${{ matrix.type }}" != "Phpunit Lowest" ] && [ "${{ matrix.type }}" != "Phpunit Burn" ]; then composer remove --no-interaction --no-update phpunit/phpunit johnkary/phpunit-speedtrap phpunit/phpcov --dev ; fi if [ "${{ matrix.type }}" != "CodingStyle" ]; then composer remove --no-interaction --no-update friendsofphp/php-cs-fixer --dev ; fi - composer remove --no-interaction --no-update 'behat/*' --dev + if [ "${{ matrix.type }}" != "StaticAnalysis" ]; then composer remove --no-interaction --no-update phpstan/phpstan 'behat/*' --dev ; fi if [ "${{ matrix.php }}" == "8.0" ]; then composer config platform.php 7.4.5 ; fi composer install --no-suggest --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader if [ "${{ matrix.type }}" == "Phpunit Lowest" ]; then composer update --ansi --prefer-dist --prefer-lowest --prefer-stable --no-interaction --no-progress --optimize-autoloader ; fi @@ -261,6 +269,7 @@ jobs: run: | composer remove --no-interaction --no-update phpunit/phpunit johnkary/phpunit-speedtrap phpunit/phpcov --dev composer remove --no-interaction --no-update friendsofphp/php-cs-fixer --dev + composer remove --no-interaction --no-update phpstan/phpstan --dev composer install --no-suggest --ansi --prefer-dist --no-interaction --no-progress --optimize-autoloader if [ "${{ matrix.type }}" == "Chrome Lowest" ]; then composer update --ansi --prefer-dist --prefer-lowest --prefer-stable --no-interaction --no-progress --optimize-autoloader ; fi diff --git a/composer.json b/composer.json index 2d218395bd..ae2a4ab9f8 100644 --- a/composer.json +++ b/composer.json @@ -78,6 +78,7 @@ "instaclick/php-webdriver": "^1.4.7", "guzzlehttp/guzzle": "^6.3", "johnkary/phpunit-speedtrap": "^3.2", + "phpstan/phpstan": "^0.12.58", "phpunit/phpcov": "*", "phpunit/phpunit": ">=9.3", "symfony/process": "^4.4 || ^5.0" diff --git a/demos/_includes/Persistence_Faker.php b/demos/_includes/Persistence_Faker.php index d1ed1366cc..b77e3a9a39 100644 --- a/demos/_includes/Persistence_Faker.php +++ b/demos/_includes/Persistence_Faker.php @@ -6,17 +6,15 @@ class Persistence_Faker extends \atk4\data\Persistence { + /** @var \Faker\Generator */ public $faker; + /** @var int */ public $count = 5; - public function __construct($opts = []) + public function __construct() { - //parent::__construct($opts); - - if (!$this->faker) { - $this->faker = \Faker\Factory::create(); - } + $this->faker = \Faker\Factory::create(); } public function prepareIterator($model) diff --git a/demos/_unit-test/exception.php b/demos/_unit-test/exception.php index b1ea7f2482..a29b822c60 100644 --- a/demos/_unit-test/exception.php +++ b/demos/_unit-test/exception.php @@ -17,7 +17,7 @@ $modal = \atk4\ui\Modal::addTo($app, ['cb' => $cb]); $modal->name = 'm_test'; -$modal->set(function ($m) use ($modal) { +$modal->set(function ($m) { throw new \Exception('TEST!'); }); @@ -27,7 +27,7 @@ $cb1 = CallbackLater::addTo($app, ['urlTrigger' => 'm2_cb']); $modal2 = \atk4\ui\Modal::addTo($app, ['cb' => $cb1]); -$modal2->set(function ($m) use ($modal2) { +$modal2->set(function ($m) { trigger_error('error triggered'); }); diff --git a/demos/_unit-test/lookup.php b/demos/_unit-test/lookup.php index 5562b1e775..90673625d2 100644 --- a/demos/_unit-test/lookup.php +++ b/demos/_unit-test/lookup.php @@ -17,7 +17,7 @@ $edit = $model->getUserAction('edit'); $edit->ui = ['execButton' => [\atk4\ui\Button::class, 'EditMe', 'blue']]; $edit->description = 'edit'; -$edit->callback = function ($model) use ($app) { +$edit->callback = function ($model) { return $model->ref('product_category_id')->getTitle() . ' - ' . $model->ref('product_sub_category_id')->getTitle(); }; diff --git a/demos/_unit-test/scope-builder.php b/demos/_unit-test/scope-builder.php index c8289e33ae..760b300072 100644 --- a/demos/_unit-test/scope-builder.php +++ b/demos/_unit-test/scope-builder.php @@ -48,9 +48,9 @@ }); $expectedWord = <<<'EOF' - Project Budget is greater or equal to '1000' - and (Project Name is regular expression '[a-zA-Z]' - and Client Country Iso is equal to 'Brazil' and Start Date is equal to '2020-10-22') + Project Budget is greater or equal to '1000' + and (Project Name is regular expression '[a-zA-Z]' + and Client Country Iso is equal to 'Brazil' and Start Date is equal to '2020-10-22') and (Finish Time is not equal to '22:22' or Is Commercial is equal to '0' or Currency is equal to 'USD') EOF; @@ -124,7 +124,7 @@ "type": "query-builder-rule", "query": { "rule": "is_commercial", - "operator": "is exactly", + "operator": "equals", "value": "0", "option": null } diff --git a/demos/form/form.php b/demos/form/form.php index a0ed67283d..3602d8a438 100644 --- a/demos/form/form.php +++ b/demos/form/form.php @@ -66,7 +66,7 @@ $form->addControl('control', [Form\Control\Calendar::class, 'type' => 'date', 'caption' => 'Date using form control: ']); $form->buttonSave->set('Compare Date'); -$form->onSubmit(function (Form $form) use ($app) { +$form->onSubmit(function (Form $form) { $message = 'field = ' . print_r($form->model->get('field'), true) . ';
control = ' . print_r($form->model->get('control'), true); $view = new \atk4\ui\Message('Date field vs control:'); $view->invokeInit(); diff --git a/demos/form/jscondform.php b/demos/form/jscondform.php index 1ad0cb15d5..ecefeff8a8 100644 --- a/demos/form/jscondform.php +++ b/demos/form/jscondform.php @@ -41,8 +41,8 @@ $formSubscribe->addControl('f_gift', [Form\Control\Dropdown::class, 'caption' => 'Gift for Women', 'values' => ['Wine Glass', 'Lipstick']]); // Show email and gender when subscribe is checked. -// Show m_gift when gender is exactly equal to 'male' and subscribe is checked. -// Show f_gift when gender is exactly equal to 'female' and subscribe is checked. +// Show m_gift when gender = 'male' and subscribe is checked. +// Show f_gift when gender = 'female' and subscribe is checked. $formSubscribe->setControlsDisplayRules([ 'email' => ['subscribe' => 'checked'], 'gender' => ['subscribe' => 'checked'], @@ -54,7 +54,7 @@ \atk4\ui\Header::addTo($app, ['Dog registration', 'size' => 2]); $formDog = Form::addTo($app, ['segment']); -\atk4\ui\Label::addTo($formDog, ['You can select type of hair cut only with race that contains "poodle" AND age no more than 5 year OR your dog race is exactly "bichon".', 'top attached'], ['AboveControls']); +\atk4\ui\Label::addTo($formDog, ['You can select type of hair cut only with race that contains "poodle" AND age no more than 5 year OR your dog race equals "bichon".', 'top attached'], ['AboveControls']); $formDog->addControl('race', [Form\Control\Line::class]); $formDog->addControl('age'); $formDog->addControl('hair_cut', [Form\Control\Dropdown::class, 'values' => ['Short', 'Long']]); diff --git a/demos/init-app.php b/demos/init-app.php index 647c2b66e7..791c2c5dbf 100644 --- a/demos/init-app.php +++ b/demos/init-app.php @@ -37,6 +37,7 @@ } try { + /** @var \atk4\data\Persistence\Sql $db */ require_once __DIR__ . '/init-db.php'; $app->db = $db; unset($db); diff --git a/demos/init-db.php b/demos/init-db.php index d853e1a8d3..7e2e739dca 100644 --- a/demos/init-db.php +++ b/demos/init-db.php @@ -169,7 +169,7 @@ protected function init(): void $this->addField('is_folder', ['type' => 'boolean']); $this->hasMany('SubFolder', [new self(), 'their_field' => 'parent_folder_id']) - ->addField('count', ['aggregate' => 'count', 'field' => $this->expr('*')]); + ->addField('count', ['aggregate' => 'count', 'field' => $this->persistence->expr($this, '*')]); $this->hasOne('parent_folder_id', Folder::class) ->addTitle(); diff --git a/demos/interactive/accordion-nested.php b/demos/interactive/accordion-nested.php index 59ed70b952..958129eb90 100644 --- a/demos/interactive/accordion-nested.php +++ b/demos/interactive/accordion-nested.php @@ -27,7 +27,7 @@ } // dynamic section - simple view - $i2 = $accordion->addSection('Dynamic Text', function ($v) use ($maxDepth, $level) { + $i2 = $accordion->addSection('Dynamic Text', function ($v) use ($addAccordionFunc, $maxDepth, $level) { \atk4\ui\Message::addTo($v, ['Every time you open this accordion item, you will see a different text', 'ui' => 'tiny message']); \atk4\ui\LoremIpsum::addTo($v, ['size' => 2]); if ($level < $maxDepth) { @@ -36,7 +36,7 @@ }); // dynamic section - form view - $i3 = $accordion->addSection('Dynamic Form', function ($v) use ($maxDepth, $level) { + $i3 = $accordion->addSection('Dynamic Form', function ($v) use ($addAccordionFunc, $maxDepth, $level) { \atk4\ui\Message::addTo($v, ['Loading a form dynamically.', 'ui' => 'tiny message']); $form = \atk4\ui\Form::addTo($v); $form->addControl('Email'); diff --git a/demos/interactive/popup.php b/demos/interactive/popup.php index baad24cd89..051c7df1d0 100644 --- a/demos/interactive/popup.php +++ b/demos/interactive/popup.php @@ -184,7 +184,7 @@ public function linkCart($cart, $jsAction = null) $cartOutterLabel->addStyle('display', 'none'); } -$cartPopup->set(function ($popup) use ($shelf, $cartOutterLabel, $cart) { +$cartPopup->set(function ($popup) use ($cart) { $cartInnerLabel = \atk4\ui\Label::addTo($popup, ['Number of items:']); // cart is already initialized, so init() is not called again. However, cart will be rendered diff --git a/docs/form.rst b/docs/form.rst index 612bedc168..fb955952d1 100644 --- a/docs/form.rst +++ b/docs/form.rst @@ -780,8 +780,8 @@ Here is a more advanced example:: // Show email and gender when subscribe is checked. - // Show m_gift when gender is exactly equal to 'male' and subscribe is checked. - // Show f_gift when gender is exactly equal to 'female' and subscribe is checked. + // Show m_gift when gender = 'male' and subscribe is checked. + // Show f_gift when gender = 'female' and subscribe is checked. $f_sub->setControlsDisplayRules([ 'email' => ['subscribe' => 'checked'], diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000000..100c61ced9 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,330 @@ +parameters: + level: 2 + paths: + - ./ + excludes_analyse: + - cache/ + - build/ + - vendor/ + + # TODO review once we drop PHP 7.x support + treatPhpDocTypesAsCertain: false + + ignoreErrors: + - '~^Unsafe usage of new static\(\)\.$~' + + # TODO these rules are generated, this ignores should be fixed in the code + # for level = 1 + # for tsrc/AbstractView.php + - '~^Access to an undefined property atk4\\ui\\AbstractView\:\:\$skin\.$~' + # for tsrc/CardDeck.php + - '~^Access to an undefined property atk4\\ui\\CardDeck\:\:\$editFields\.$~' + # for tsrc/Console.php + - '~^Variable \$old_logger might not be defined\.$~' + - '~^Variable \$stat might not be defined\.$~' + - '~^Variable \$loggerBak might not be defined\.$~' + - '~^Variable \$debugBak might not be defined\.$~' + # for tsrc/Form/Control/Calendar.php + - '~^Class atk4\\ui\\JsFunction constructor invoked with 3 parameters, 0\-2 required\.$~' + # for tsrc/Form/Control/Lookup.php + - '~^Call to an undefined method atk4\\ui\\Form\\Control\\Lookup\:\:search\(\)\.$~' + # for tsrc/Form/Control/ScopeBuilder.php + - '~^Variable \$inputType might not be defined\.$~' + # for tsrc/Form/Control/Upload.php + - '~^Function array_flip invoked with 2 parameters, 1 required\.$~' + # for tsrc/HtmlTemplate.php + - '~^Method atk4\\ui\\HtmlTemplate\:\:del\(\) invoked with 2 parameters, 1 required\.$~' + # for tsrc/Modal.php + - '~^Access to an undefined property atk4\\ui\\Modal\:\:\$options\.$~' + # for tsrc/Table.php + - '~^Access to an undefined property atk4\\ui\\Table\:\:\$t_row_master\.$~' + - '~^Access to an undefined property atk4\\ui\\Table\:\:\$model¨\.$~' + # for tsrc/Table/Column/Delete.php + - '~^Access to an undefined property atk4\\ui\\Table\\Column\\Delete\:\:\$vp\.$~' + # for tsrc/Table/Column/FilterModel/TypeTime.php + - '~^Call to an undefined method atk4\\ui\\Table\\Column\\FilterModel\\TypeTime\:\:recalData\(\)\.$~' + # for tsrc/View.php + - '~^Method atk4\\ui\\View\:\:setModel\(\) invoked with 2 parameters, 1 required\.$~' + # for tsrc/Wizard.php + - '~^Access to an undefined property atk4\\ui\\Wizard\:\:\$stepTemplate\.$~' + - '~^Access to an undefined property atk4\\ui\\Wizard\:\:\$buttonFinish\.$~' + # for tests/CallbackTest.php + - '~^Access to an undefined property atk4\\ui\\tests\\AppMock\:\:\$terminate\.$~' + + # TODO these rules are generated, this ignores should be fixed in the code + # for level = 2 + # for demos/_includes/Counter.php + - '~^Method atk4\\ui\\Jquery\:\:val\(\) invoked with 1 parameter, 0 required\.$~' + # for demos/_includes/Demo.php + - '~^Call to an undefined method atk4\\ui\\JsChain\:\:initHighlighting\(\)\.$~' + # for demos/_includes/DemoLookup.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:modal\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:dropdown\(\)\.$~' + # for demos/_unit-test/callback.php + - '~^Access to an undefined property atk4\\ui\\AbstractView\:\:\$cb\.$~' + - '~^Call to an undefined method atk4\\ui\\AbstractView\:\:setModel\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\AbstractView\:\:jsReload\(\)\.$~' + # for demos/_unit-test/scope-builder-to-query.php + - '~^Method atk4\\ui\\App\:\:decodeJson\(\) invoked with 2 parameters, 1 required\.$~' + # for demos/basic/label.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:fadeOut\(\)\.$~' + # for demos/basic/message.php + - '~^Method atk4\\ui\\Jquery\:\:attr\(\) invoked with 1 parameter, 0 required\.$~' + - '~^Method atk4\\ui\\Jquery\:\:find\(\) invoked with 1 parameter, 0 required\.$~' + # for demos/basic/view.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:rating\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:transition\(\)\.$~' + # for demos/collection/crud.php + - '~^Method atk4\\ui\\Columns\:\:addColumn\(\) invoked with 2 parameters, 0\-1 required\.$~' + # for demos/collection/grid.php + - '~^Method atk4\\ui\\Jquery\:\:closest\(\) invoked with 1 parameter, 0 required\.$~' + # for demos/collection/jssortable.php + - '~^Call to method get\(\) on an unknown class atk4\\ui\\Model\.$~' + - '~^Call to method set\(\) on an unknown class atk4\\ui\\Model\.$~' + - '~^Call to an undefined method atk4\\ui\\Table\\Column\:\:onReorder\(\)\.$~' + # for demos/collection/lister-ipp.php + # for demos/collection/multitable.php + - '~^Method atk4\\ui\\Jquery\:\:addClass\(\) invoked with 1 parameter, 0 required\.$~' + # for demos/collection/table.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:reload\(\)\.$~' + # for demos/collection/table2.php + - '~^Call to method getId\(\) on an unknown class atk4\\ui\\Model\.$~' + # for demos/collection/tablefilter.php + - '~^Call to an undefined method atk4\\ui\\demo\\CountryLock\:\:expr\(\)\.$~' + # for demos/data-action/jsactions.php + - '~^Call to an undefined method atk4\\ui\\View\:\:addFields\(\)\.$~' + # for demos/form-control/calendar.php + - '~^Call to an undefined method atk4\\ui\\Form\\Control\:\:addAction\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Form\\Control\:\:getJsInstance\(\)\.$~' + # for demos/form-control/checkbox.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:checkbox\(\)\.$~' + # for demos/form-control/input2.php + - '~^Call to an undefined method atk4\\ui\\Form\\Control\:\:onDelete\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Form\\Control\:\:onUpload\(\)\.$~' + # for demos/form-control/multiline.php + - '~^Call to an undefined method atk4\\ui\\Form\\Layout\:\:addColumn\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Form\\Control\:\:onLineChange\(\)\.$~' + - '~^Access to an undefined property atk4\\ui\\Form\\Control\:\:\$jsAfterAdd\.$~' + - '~^Access to an undefined property atk4\\ui\\Form\\Control\:\:\$jsAfterDelete\.$~' + - '~^Call to an undefined method atk4\\ui\\Form\\Control\:\:saveRows\(\)\.$~' + # for demos/form-control/upload.php + - '~^Access to an undefined property atk4\\ui\\Form\\Control\:\:\$cb\.$~' + - '~^Call to an undefined method atk4\\ui\\Form\\Control\:\:clearThumbnail\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Form\\Control\:\:setThumbnailSrc\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Form\\Control\:\:setFileId\(\)\.$~' + # for demos/form/form-section-accordion.php + - '~^Call to an undefined method atk4\\ui\\Form\\Layout\:\:addSection\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Form\\Layout\:\:activate\(\)\.$~' + # for demos/form/form-section.php + - '~^Call to an undefined method atk4\\ui\\Form\\Layout\:\:addTab\(\)\.$~' + # for demos/form/form.php + - '~^Call to an undefined method atk4\\ui\\JsChain\:\:val\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\JsChain\:\:checkbox\(\)\.$~' + # for demos/form/form2.php + - '~^Access to an undefined property atk4\\ui\\Form\\Control\:\:\$iconLeft\.$~' + # for demos/init-app.php + - '~^Access to an undefined property atk4\\ui\\Layout&atk4\\ui\\Layout\\NavigableInterface\:\:\$menu\.$~' + # for demos/init-db.php + - '~^Call to an undefined method atk4\\data\\Reference\\HasOne\:\:addField\(\)\.$~' + - '~^Call to an undefined method atk4\\data\\Persistence\:\:expr\(\)\.$~' + - '~^Call to an undefined method atk4\\data\\Reference\\HasOne\:\:addTitle\(\)\.$~' + - '~^Call to an undefined method atk4\\data\\Model\:\:importFromFilesystem\(\)\.$~' + # for demos/interactive/modal.php + - '~^Method atk4\\ui\\Jquery\:\:removeClass\(\) invoked with 1 parameter, 0 required\.$~' + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:atkReloadView\(\)\.$~' + # for demos/interactive/popup.php + - '~^Access to an undefined property atk4\\ui\\Lister\:\:\$items\.$~' + - '~^Call to an undefined method atk4\\ui\\View\:\:linkCart\(\)\.$~' + - '~^Method atk4\\ui\\Jquery\:\:toggleClass\(\) invoked with 1 parameter, 0 required\.$~' + # for demos/interactive/scroll-container.php + # for demos/interactive/scroll-lister.php + # for demos/interactive/sse.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:atkServerEvent\(\)\.$~' + # for demos/interactive/tabs.php + - '~^Call to an undefined method atk4\\ui\\View\:\:setActive\(\)\.$~' + # for demos/interactive/wizard.php + - '~^Access to an undefined property atk4\\ui\\Form\\Control\:\:\$placeholder\.$~' + # for demos/javascript/js.php + - '~^Method atk4\\ui\\Jquery\:\:hide\(\) invoked with 1 parameter, 0 required\.$~' + # for demos/javascript/vue-component.php + # for demos/layout/layout-panel.php + - '~^Call to an undefined method atk4\\ui\\Panel\\Loadable\:\:jsOpen\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Panel\\Loadable\:\:onOpen\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Panel\\Loadable\:\:addConfirmation\(\)\.$~' + # for demos/layout/layouts.php + - '~^Method atk4\\ui\\Jquery\:\:attr\(\) invoked with 2 parameters, 0 required\.$~' + # for src/Accordion.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:accordion\(\)\.$~' + # for src/Card.php + - '~^Call to an undefined method atk4\\ui\\AbstractView\:\:addFields\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:parents\(\)\.$~' + - '~^PHPDoc tag @param has invalid value \(\[\] \$args The action argument\)\: Unexpected token "\[", expected type at offset 110$~' + - '~^Call to an undefined method atk4\\ui\\View\:\:addDescription\(\)\.$~' + - '~^PHPDoc tag @param references unknown parameter\: \$isFluid$~' + # for src/CardDeck.php + - '~^Access to an undefined property atk4\\ui\\AbstractView\:\:\$reload\.$~' + - '~^Access to an undefined property atk4\\ui\\AbstractView\:\:\$queryArg\.$~' + - '~^Call to an undefined method atk4\\ui\\AbstractView\:\:addClass\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\AbstractView\:\:on\(\)\.$~' + # for src/CardSection.php + # for src/Console.php + - '~^Method atk4\\ui\\Jquery\:\:append\(\) invoked with 1 parameter, 0 required\.$~' + - '~^Call to an undefined method object\:\:issetApp\(\)\.$~' + - '~^Call to an undefined method object\:\:getApp\(\)\.$~' + - '~^Access to an undefined property object\:\:\$debug\.$~' + # for src/Crud.php + - '~^Call to an undefined method atk4\\ui\\UserAction\\JsExecutorInterface\:\:stickyGet\(\)\.$~' + - '~^Method atk4\\ui\\UserAction\\JsExecutorInterface\:\:jsExecute\(\) invoked with 0 parameters, 1 required\.$~' + # for src/Dropdown.php + # for src/Form.php + - '~^Method atk4\\ui\\Jquery\:\:form\(\) invoked with 3 parameters, 0\-1 required\.$~' + - '~^Method atk4\\ui\\Jquery\:\:form\(\) invoked with 2 parameters, 0\-1 required\.$~' + - '~^Call to an undefined method atk4\\ui\\JsChain\:\:preventFormLeave\(\)\.$~' + # for src/Form/Control/Calendar.php + - '~^Access to an undefined property atk4\\ui\\JsChain\:\:\$l10ns\.$~' + - '~^Call to an undefined method atk4\\ui\\JsChain\:\:localize\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:flatpickr\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:get\(\)\.$~' + # for src/Form/Control/Checkbox.php + - '~^Method atk4\\ui\\Form\\Control\\Checkbox\:\:set\(\) should return \$this\(atk4\\ui\\Form\\Control\\Checkbox\) but return statement is missing\.$~' + - '~^Return typehint of method atk4\\ui\\Form\\Control\\Checkbox\:\:jsChecked\(\) has invalid type atk4\\ui\\Form\\Control\\Jquery\.$~' + # for src/Form/Control/Dropdown.php + # for src/Form/Control/DropdownCascade.php + - '~^Method atk4\\data\\Field\:\:get\(\) invoked with 1 parameter, 0 required\.$~' + - '~^PHPDoc tag @param has invalid value \(\$value the current field value\)\: Unexpected token "\$value", expected type at offset 162$~' + - '~^PHPDoc tag @param has invalid value \(\$values an array of possible values\)\: Unexpected token "\$values", expected type at offset 109$~' + # for src/Form/Control/Lookup.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:serialize\(\)\.$~' + # for src/Form/Control/Multiline.php + - '~^Property atk4\\ui\\Form\\Control\\Multiline\:\:\$jsAfterAdd has unknown class atk4\\ui\\Form\\Control\\JsFunction as its type\.$~' + - '~^Property atk4\\ui\\Form\\Control\\Multiline\:\:\$jsAfterDelete has unknown class atk4\\ui\\Form\\Control\\JsFunction as its type\.$~' + - '~^PHPDoc tag @return has invalid value \(\|null\)\: Unexpected token "\|", expected type at offset 153$~' + - '~^Class atk4\\core\\Exception referenced with incorrect case\: atk4\\Core\\Exception\.$~' + # for src/Form/Control/Radio.php + - '~^Property atk4\\ui\\Form\\Control\\Radio\:\:\$lister has unknown class atk4\\ui\\Form\\Control\\Lister as its type\.$~' + - '~^Call to method setModel\(\) on an unknown class atk4\\ui\\Form\\Control\\Lister\.$~' + - '~^Call to method onHook\(\) on an unknown class atk4\\ui\\Form\\Control\\Lister\.$~' + # for src/Form/Control/Upload.php + - '~^Property atk4\\ui\\Form\\Control\\Upload\:\:\$action has unknown class atk4\\ui\\Form\\Control\\View as its type\.$~' + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:atkFileUpload\(\)\.$~' + - '~^Access to property \$name on an unknown class atk4\\ui\\Form\\Control\\View\.$~' + # for src/Form/Control/UploadImage.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:removeAttr\(\)\.$~' + # for src/Form/Layout.php + # for src/Form/Layout/Custom.php + - '~^PHPDoc tag @var has invalid value \(\{@inheritdoc\}\)\: Unexpected token "\{", expected type at offset 9$~' + # for src/Grid.php + - '~^Call to an undefined method atk4\\ui\\Table\\Column\:\:addActionMenuItem\(\)\.$~' + # for src/ItemsPerPageSelector.php + # for src/JsCallback.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:atkAjaxec\(\)\.$~' + - '~^Cannot access property \$_chain on string\.$~' + # for src/JsConditionalForm.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:atkConditionalForm\(\)\.$~' + # for src/JsPaginator.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:atkScroll\(\)\.$~' + # for src/JsReload.php + # for src/JsSearch.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:atkJsSearch\(\)\.$~' + # for src/JsSortable.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:atkJsSortable\(\)\.$~' + # for src/JsSse.php + # for src/JsToast.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:toast\(\)\.$~' + # for src/JsVueService.php + - '~^Call to an undefined method atk4\\ui\\JsChain\:\:createAtkVue\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\JsChain\:\:createVue\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\JsChain\:\:useComponent\(\)\.$~' + # for src/Layout/Admin.php + - '~^PHPDoc tag @param has invalid value \(\$seed\)\: Unexpected token "\$seed", expected type at offset 58$~' + # for src/Layout/Maestro.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:atkSidenav\(\)\.$~' + # for src/Layout/NavigableInterface.php + # for src/Lister.php + - '~^Property atk4\\ui\\Lister\:\:\$current_row has unknown class atk4\\ui\\Model as its type\.$~' + # for src/Loader.php + - '~^Default value of the parameter \#1 \$fx \(array\(\)\) of method atk4\\ui\\Loader\:\:set\(\) is incompatible with type Closure\.$~' + # for src/Menu.php + - '~^PHPDoc tag @var has invalid value \(\[type\]\)\: Unexpected token "\[", expected type at offset 118$~' + - '~^Call to an undefined method atk4\\ui\\AbstractView\:\:setElement\(\)\.$~' + # for src/Modal.php + - '~^Default value of the parameter \#1 \$fx \(array\(\)\) of method atk4\\ui\\Modal\:\:set\(\) is incompatible with type Closure\.$~' + # for src/Panel/Right.php + - '~^PHPDoc tag @return with type mixed is not subtype of native type atk4\\ui\\JsExpression\.$~' + - '~^Call to an undefined method atk4\\ui\\JsExpression\:\:openPanel\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\JsExpression\:\:reloadPanel\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\JsExpression\:\:closePanel\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\AbstractView\:\:addButtonAction\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\AbstractView\:\:notClosable\(\)\.$~' + - '~^PHPDoc tag @param references unknown parameter\: \$selector$~' + - '~^Call to an undefined method atk4\\ui\\Panel\\LoadableContent\:\:getClearSelector\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\JsExpression\:\:addPanel\(\)\.$~' + # for src/Popup.php + - '~^Method atk4\\ui\\Popup\:\:set\(\) should return \$this\(atk4\\ui\\Popup\) but return statement is missing\.$~' + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:popup\(\)\.$~' + # for src/ProgressBar.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:progress\(\)\.$~' + # for src/Tab.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:tab\(\)\.$~' + - '~^Access to an undefined property atk4\\ui\\View\:\:\$activeTabName\.$~' + # for src/Table.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:atkColumnResizer\(\)\.$~' + - '~^Method atk4\\ui\\Jquery\:\:css\(\) invoked with 2 parameters, 0 required\.$~' + # for src/Table/Column.php + - '~^Call to an undefined method atk4\\ui\\AbstractView\:\:setHoverable\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\JsCallback\:\:onSelectItem\(\)\.$~' + # for src/Table/Column/ActionMenu.php + # for src/Table/Column/Checkbox.php + # for src/Table/Column/Delete.php + - '~^Call to an undefined method atk4\\ui\\AbstractView\:\:set\(\)\.$~' + # for src/Table/Column/DragHandler.php + - '~^Call to an undefined method atk4\\ui\\JsCallback\:\:onReorder\(\)\.$~' + # for src/Table/Column/FilterModel.php + - '~^PHPDoc tag @var has invalid value \(\)\: Unexpected token "\\n ", expected type at offset 79$~' + - '~^PHPDoc tag @var has invalid value \(\)\: Unexpected token "\\n ", expected type at offset 76$~' + - '~^PHPDoc tag @param references unknown parameter\: \$persistence$~' + # for src/Table/Column/FilterPopup.php + - '~^Method atk4\\ui\\Jquery\:\:trigger\(\) invoked with 1 parameter, 0 required\.$~' + - '~^Call to an undefined method atk4\\data\\Model\:\:recallData\(\)\.$~' + - '~^Call to an undefined method atk4\\data\\Model\:\:setConditionForModel\(\)\.$~' + # for src/Table/Column/Link.php + - '~^Method atk4\\ui\\Table\\Column\\Link\:\:setDefaults\(\) should return \$this\(atk4\\ui\\Table\\Column\\Link\) but return statement is missing\.$~' + - '~^Cannot call method set\(\) on array\|string\.$~' + # for src/Tabs.php + - '~^Call to an undefined method atk4\\ui\\View\:\:setPath\(\)\.$~' + # for src/TabsSubview.php + # for src/UserAction/ConfirmationExecutor.php + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:off\(\)\.$~' + - '~^PHPDoc tag @param has invalid value \(\$id\)\: Unexpected token "\$id", expected type at offset 99$~' + - '~^PHPDoc tag @param has invalid value \(\$obj\)\: Unexpected token "\$obj", expected type at offset 80$~' + # for src/UserAction/ModalExecutor.php + - '~^PHPDoc tag @return with type atk4\\ui\\Form\|null is not subtype of native type atk4\\ui\\Form\.$~' + - '~^Access to an undefined property atk4\\ui\\AbstractView\:\:\$buttonSave\.$~' + # for src/View.php + - '~^Call to an undefined method atk4\\ui\\JsChain\:\:emit\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\JsChain\:\:clearData\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\JsChain\:\:addJsonData\(\)\.$~' + - '~^Access to an undefined property atk4\\ui\\UserAction\\JsExecutorInterface&atk4\\ui\\View\:\:\$viewForUrl\.$~' + - '~^Call to an undefined method atk4\\ui\\AbstractView\:\:setAction\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\App\:\:jsReady\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\App\:\:getViewJS\(\)\.$~' + # for src/VirtualPage.php + - '~^Method atk4\\ui\\VirtualPage\:\:getHtml\(\) should return string but return statement is missing\.$~' + # for tests-behat/bootstrap/Context.php + - '~^PHPDoc tag @param has invalid value \(\$arg1\)\: Unexpected token "\$arg1", expected type at offset 100$~' + - '~^PHPDoc tag @param has invalid value \(\$arg1\)\: Unexpected token "\$arg1", expected type at offset 68$~' + - '~^PHPDoc tag @param has invalid value \(\$arg\)\: Unexpected token "\$arg", expected type at offset 64$~' + - '~^PHPDoc tag @param has invalid value \(\$arg1\)\: Unexpected token "\$arg1", expected type at offset 83$~' + - '~^PHPDoc tag @param has invalid value \(\$arg2\)\: Unexpected token "\$arg2", expected type at offset 103$~' + - '~^PHPDoc tag @param has invalid value \(\$arg1\)\: Unexpected token "\$arg1", expected type at offset 79$~' + # for tests/DemosTest.php + - '~^Variable \$app in PHPDoc tag @var does not exist\.$~' + # for tests/FormTest.php + - '~^Access to an undefined property atk4\\ui\\App\:\:\$output\.$~' + # for tests/jsTest.php + - '~^Call to an undefined method atk4\\ui\\JsChain\:\:getTextInRange\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:first\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:ready\(\)\.$~' + - '~^Call to an undefined method atk4\\ui\\Jquery\:\:height\(\)\.$~' + # for tools/get-assets.php + - '~^Method GetAssets\:\:requireJs\(\) should return \$this\(GetAssets\) but return statement is missing\.$~' diff --git a/src/Callback.php b/src/Callback.php index d0b0840ec8..f78c9e6531 100644 --- a/src/Callback.php +++ b/src/Callback.php @@ -56,7 +56,7 @@ public function getUrlTrigger(): string * @param \Closure $fx * @param array $args * - * @return mixed|null + * @return mixed */ public function set($fx = null, $args = null) { diff --git a/src/CallbackLater.php b/src/CallbackLater.php index c14588b707..fb67e8cd75 100644 --- a/src/CallbackLater.php +++ b/src/CallbackLater.php @@ -19,7 +19,7 @@ class CallbackLater extends Callback * @param \Closure $fx * @param array $args * - * @return mixed|null + * @return mixed */ public function set($fx = null, $args = null) { diff --git a/src/CardDeck.php b/src/CardDeck.php index ddca4036d2..6208eb3aab 100644 --- a/src/CardDeck.php +++ b/src/CardDeck.php @@ -294,7 +294,7 @@ protected function jsModelReturn(Model\UserAction $action = null, string $msg = * Therefore if card, that was just save, is not present in db result set or deck then return null * otherwise return Card view. * - * @return mixed|null + * @return mixed */ protected function findCard(Model $model) { diff --git a/src/Crud.php b/src/Crud.php index 5bca7c1d23..04b6af73ce 100644 --- a/src/Crud.php +++ b/src/Crud.php @@ -359,7 +359,7 @@ public function onFormAddEdit(\Closure $fx) /** * Set onActions. * - * @return mixed|null + * @return mixed */ public function setOnActions(string $actionName, \Closure $fx) { diff --git a/src/Form.php b/src/Form.php index 53f3dd3986..b0c1a04a9c 100644 --- a/src/Form.php +++ b/src/Form.php @@ -378,7 +378,7 @@ public function success($success = 'Success', $sub_header = null, $useTemplate = $response = $this->js()->html($response->renderToHtml()); } else { $response = new Message([$success, 'type' => 'success', 'icon' => 'check']); - $response->setApp($this->getApp); + $response->setApp($this->getApp()); $response->invokeInit(); $response->text->addParagraph($sub_header); } diff --git a/src/Form/Control/Lookup.php b/src/Form/Control/Lookup.php index 0798f1eaa7..bd8306a97b 100644 --- a/src/Form/Control/Lookup.php +++ b/src/Form/Control/Lookup.php @@ -5,6 +5,7 @@ namespace atk4\ui\Form\Control; use atk4\core\Factory; +use atk4\data\Model; use atk4\ui\Jquery; use atk4\ui\JsExpression; use atk4\ui\JsFunction; @@ -204,7 +205,7 @@ public function getData($limit = true): array /** * Renders the Lookup row depending on properties set. */ - public function renderRow(\atk4\data\Model $row): array + public function renderRow(Model $row): array { $renderRowFunction = $this->renderRowFunction ?? \Closure::fromCallable([static::class, 'defaultRenderRow']); @@ -219,7 +220,7 @@ public function renderRow(\atk4\data\Model $row): array * * @return string[] */ - public static function defaultRenderRow($field, \atk4\data\Model $row, $key = null) + public static function defaultRenderRow($field, Model $row, $key = null) { $id_field = $field->id_field ?: $row->id_field; $title_field = $field->title_field ?: $row->title_field; @@ -376,7 +377,7 @@ public function setApiConfig($config) /** * Override this method if you want to add more logic to the initialization of the auto-complete field. * - * @param Jquery + * @param Jquery $chain */ protected function initDropdown($chain) { diff --git a/src/Form/Control/ScopeBuilder.php b/src/Form/Control/ScopeBuilder.php index 98dc27546b..daa947e132 100644 --- a/src/Form/Control/ScopeBuilder.php +++ b/src/Form/Control/ScopeBuilder.php @@ -113,7 +113,7 @@ class ScopeBuilder extends Control */ protected $query = []; - protected const OPERATOR_TEXT_EQUALS = 'is exactly'; + protected const OPERATOR_TEXT_EQUALS = 'equals'; protected const OPERATOR_TEXT_DOESNOT_EQUAL = 'does not equal'; protected const OPERATOR_TEXT_GREATER = 'is alphabetically after'; protected const OPERATOR_TEXT_GREATER_EQUAL = 'is alphabetically equal or after'; @@ -205,8 +205,6 @@ class ScopeBuilder extends Control self::OPERATOR_TEXT_DOESNOT_BEGIN_WITH => Condition::OPERATOR_NOT_LIKE, self::OPERATOR_TEXT_ENDS_WITH => Condition::OPERATOR_LIKE, self::OPERATOR_TEXT_DOESNOT_END_WITH => Condition::OPERATOR_NOT_LIKE, - self::OPERATOR_EQUALS => Condition::OPERATOR_EQUALS, - self::OPERATOR_DOESNOT_EQUAL => Condition::OPERATOR_DOESNOT_EQUAL, self::OPERATOR_IN => Condition::OPERATOR_IN, self::OPERATOR_NOT_IN => Condition::OPERATOR_NOT_IN, self::OPERATOR_TEXT_MATCHES_REGEX => Condition::OPERATOR_REGEXP, diff --git a/src/Grid.php b/src/Grid.php index 6f1f8f8781..2b0631ab62 100644 --- a/src/Grid.php +++ b/src/Grid.php @@ -249,7 +249,7 @@ public function addItemsPerPageSelector($items = [10, 25, 50, 100], $label = 'It $pageLength->stickyGet($this->sortTrigger, $sortBy); } - $pageLength->onPageLengthSelect(function ($ipp) use ($pageLength) { + $pageLength->onPageLengthSelect(function ($ipp) { $this->ipp = $ipp; $this->setModelLimitFromPaginator(); // add ipp to quicksearch diff --git a/src/UserAction/ModalExecutor.php b/src/UserAction/ModalExecutor.php index 7cb08c9c79..4c51a92133 100644 --- a/src/UserAction/ModalExecutor.php +++ b/src/UserAction/ModalExecutor.php @@ -294,7 +294,7 @@ protected function doArgs(View $modal) $this->jsSetSubmitBtn($modal, $form, $this->step); $this->jsSetPrevHandler($modal, $this->step); - $form->onSubmit(function (Form $form) use ($modal) { + $form->onSubmit(function (Form $form) { // collect arguments. $this->actionData['args'] = $form->model->get(); diff --git a/tests/DemosTest.php b/tests/DemosTest.php index 4803e59662..f9300f189f 100644 --- a/tests/DemosTest.php +++ b/tests/DemosTest.php @@ -58,9 +58,11 @@ protected function setUp(): void throw new \atk4\ui\Exception('Demos init must setup only $app variable'); } + // @phpstan-ignore-next-line remove once https://github.com/phpstan/phpstan/issues/4155 is resolved self::$_db = $app->db; // prevent $app to run on shutdown + // @phpstan-ignore-next-line remove once https://github.com/phpstan/phpstan/issues/4155 is resolved $app->run_called = true; } } diff --git a/tests/FormTest.php b/tests/FormTest.php index 20f614d26c..8a69855b74 100644 --- a/tests/FormTest.php +++ b/tests/FormTest.php @@ -131,7 +131,7 @@ public function assertFormControlError(string $field, string $error) public function assertFromControlNoErrors(string $field) { - preg_replace_callback('/form\("add prompt","([^"]*)","([^"]*)"\)/', function ($matches) use ($field, &$matched) { + preg_replace_callback('/form\("add prompt","([^"]*)","([^"]*)"\)/', function ($matches) use ($field) { if ($matches[1] === $field) { $this->fail('Form control ' . $field . ' unexpected error: ' . $matches[2]); } diff --git a/tools/get-assets.php b/tools/get-assets.php index d918ce2ffc..a574a6f9ad 100644 --- a/tools/get-assets.php +++ b/tools/get-assets.php @@ -9,7 +9,7 @@ class GetAssets extends \atk4\ui\App public $always_run = false; public $catch_exceptions = false; - public function requireJs($path) + public function requireJs($path, $isAsync = false, $isDefer = false) { $file = 'public/' . basename($path); echo "Downloading {$path} into {$file}..\n";