diff --git a/.github/workflows/test-unit.yml b/.github/workflows/test-unit.yml index e0f247e2..fa9c6a92 100644 --- a/.github/workflows/test-unit.yml +++ b/.github/workflows/test-unit.yml @@ -85,7 +85,7 @@ jobs: - php: 'latest' type: 'Phpunit Burn' env: - LOG_COVERAGE: "${{ fromJSON('{true: \"1\", false: \"\"}')[matrix.php == '8.2' && matrix.type == 'Phpunit' && (github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')))] }}" + LOG_COVERAGE: "${{ fromJSON('{true: \"1\", false: \"\"}')[matrix.php == '8.3' && matrix.type == 'Phpunit' && (github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')))] }}" services: mysql: image: mysql @@ -139,7 +139,7 @@ jobs: if [ -n "$LOG_COVERAGE" ]; then composer require --no-interaction --no-install phpunit/phpcov; fi composer update --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 - if [ "${{ matrix.type }}" = "Phpunit Burn" ]; then sed -i 's~ *public function runBare(): void~public function runBare(): void { gc_collect_cycles(); gc_collect_cycles(); $memDiffs = array_fill(0, '"$(if [ \"$GITHUB_EVENT_NAME\" == \"schedule\" ]; then echo 64; else echo 16; fi)"', 0); for ($i = -1; $i < count($memDiffs); ++$i) { $this->_runBare(); gc_collect_cycles(); gc_collect_cycles(); $mem = memory_get_usage(); if ($i !== -1) { $memDiffs[$i] = $mem - $memPrev; } $memPrev = $mem; rsort($memDiffs); if (array_sum($memDiffs) >= 4096 * 1024 || $memDiffs[2] > 0) { $this->onNotSuccessfulTest(new AssertionFailedError( "Memory leak detected! (" . implode(" + ", array_map(fn ($v) => number_format($v / 1024, 3, ".", " "), array_filter($memDiffs))) . " KB, " . ($i + 2) . " iterations)" )); } } } private function _runBare(): void~' vendor/phpunit/phpunit/src/Framework/TestCase.php && cat vendor/phpunit/phpunit/src/Framework/TestCase.php | grep '_runBare('; fi + if [ "${{ matrix.type }}" = "Phpunit Burn" ]; then sed -i 's~public function runBare(): void~public function runBare(): void { gc_collect_cycles(); $memDiffs = array_fill(0, '"$(if [ \"$GITHUB_EVENT_NAME\" == \"schedule\" ]; then echo 64; else echo 16; fi)"', 0); $emitter = Event\\Facade::emitter(); for ($i = -1; $i < count($memDiffs); ++$i) { $this->_runBare(); if ($this->inIsolation) { $dispatcher = \\Closure::bind(static fn () => $emitter->dispatcher, null, Event\\DispatchingEmitter::class)(); if ($i === -1) { $dispatcherEvents = $dispatcher->flush()->asArray(); } else { $dispatcher->flush(); } foreach ($dispatcherEvents as $event) { $dispatcher->dispatch($event); } } gc_collect_cycles(); $mem = memory_get_usage(); if ($i !== -1) { $memDiffs[$i] = $mem - $memPrev; } $memPrev = $mem; rsort($memDiffs); if (array_sum($memDiffs) >= 4096 * 1024 || $memDiffs[2] > 0) { $e = new AssertionFailedError("Memory leak detected! (" . implode(" + ", array_map(static fn ($v) => number_format($v / 1024, 3, ".", " "), array_filter($memDiffs))) . " KB, " . ($i + 2) . " iterations)"); $this->status = TestStatus::failure($e->getMessage()); $emitter->testFailed($this->valueObjectForEvents(), Event\\Code\\ThrowableBuilder::from($e), Event\\Code\\ComparisonFailureBuilder::from($e)); $this->onNotSuccessfulTest($e); } } } private function _runBare(): void~' vendor/phpunit/phpunit/src/Framework/TestCase.php && cat vendor/phpunit/phpunit/src/Framework/TestCase.php | grep '_runBare('; fi - name: Init run: | @@ -151,7 +151,7 @@ jobs: - name: "Run tests: SQLite" run: | - php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) -v + php -d opcache.enable_cli=1 vendor/bin/phpunit --exclude-group none $(if [ -n "$LOG_COVERAGE" ]; then echo --coverage-text; else echo --no-coverage; fi) --fail-on-warning --fail-on-risky $(if vendor/bin/phpunit --version | grep -q '^PHPUnit 9\.'; then echo -v; else echo --fail-on-notice --fail-on-deprecation --display-notices --display-deprecations --display-warnings --display-errors --display-incomplete --display-skipped; fi) if [ -n "$LOG_COVERAGE" ]; then mv coverage/phpunit.cov coverage/phpunit-sqlite.cov; fi - name: "Run tests: MySQL" @@ -238,7 +238,7 @@ jobs: - php: 'latest' type: 'Firefox Slow' env: - LOG_COVERAGE: "${{ fromJSON('{true: \"1\", false: \"\"}')[matrix.php == '8.2' && matrix.type == 'Chrome' && (github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')))] }}" + LOG_COVERAGE: "${{ fromJSON('{true: \"1\", false: \"\"}')[matrix.php == '8.3' && matrix.type == 'Chrome' && (github.event_name == 'pull_request' || (github.event_name == 'push' && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/master')))] }}" services: mysql: image: mysql diff --git a/composer.json b/composer.json index e02ef60b..16008a92 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ }, "require-release": { "php": ">=7.4 <8.4", - "atk4/ui": "~5.0.0" + "atk4/ui": "~5.1.0" }, "require-dev": { "atk4/behat-mink-selenium2-driver": "^1.6.2", @@ -45,7 +45,7 @@ "phpstan/phpstan": "^1.0", "phpstan/phpstan-deprecation-rules": "^1.0", "phpstan/phpstan-strict-rules": "^1.3", - "phpunit/phpunit": "^9.5.5" + "phpunit/phpunit": "^9.5.5 || ^10.0" }, "conflict": { "behat/behat": "<3.9", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 70c8c501..2f30eba1 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -15,35 +15,31 @@ parameters: - '~^Only booleans are allowed in .+, .+ given( on the (left|right) side)?\.~' # TODO these rules are generated, this ignores should be fixed in the code - #- - # message: '~^Call to an undefined method Atk4\\Ui\\Form\\Control::addAction\(\)\.$~' - # count: 1 - # path: src/Form/Login.php - #- - # message: '~^Call to an undefined method Atk4\\Ui\\Form\\Control::setInputAttr\(\)\.$~' - # count: 2 - # path: src/Form/Register.php - #- - # message: '~^Call to an undefined method Atk4\\Data\\Reference\\HasOne::addTitle\(\)\.$~' - # count: 1 - # path: src/Model/AccessRule.php - #- - # message: '~^Call to an undefined method Atk4\\Data\\Reference\\HasOne::addTitle\(\)\.$~' - # count: 1 - # path: src/Model/User.php - #- - # message: '~^Call to an undefined method Atk4\\Ui\\Table\\Column::addModal\(\)\.$~' - # count: 1 - # path: src/RoleAdmin.php - #- - # message: '~^Call to an undefined method Atk4\\Ui\\Table\\Column::addModal\(\)\.$~' - # count: 1 - # path: src/UserAdmin.php - #- - # message: '~^Call to an undefined method Atk4\\Ui\\Form\\Control::addAction\(\)\.$~' - # count: 1 - # path: src/UserAdmin.php - #- - # message: '~^Call to an undefined method Atk4\\Ui\\View::jsHide\(\)\.$~' - # count: 1 - # path: src/UserAdmin.php + - + message: '~^Call to an undefined method Atk4\\Ui\\Form\\Control::addAction\(\)\.$~' + count: 1 + path: src/Form/Login.php + - + message: '~^Call to an undefined method Atk4\\Ui\\Form\\Control::setInputAttr\(\)\.$~' + count: 2 + path: src/Form/Register.php + - + message: '~^Call to an undefined method Atk4\\Data\\Reference\\HasOne::addTitle\(\)\.$~' + count: 1 + path: src/Model/AccessRule.php + - + message: '~^Call to an undefined method Atk4\\Data\\Reference\\HasOne::addTitle\(\)\.$~' + count: 1 + path: src/Model/User.php + - + message: '~^Call to an undefined method Atk4\\Ui\\Table\\Column::addModal\(\)\.$~' + count: 1 + path: src/RoleAdmin.php + - + message: '~^Call to an undefined method Atk4\\Ui\\Table\\Column::addModal\(\)\.$~' + count: 1 + path: src/UserAdmin.php + - + message: '~^Call to an undefined method Atk4\\Ui\\Form\\Control::addAction\(\)\.$~' + count: 1 + path: src/UserAdmin.php diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 95cbd935..8e6151da 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -19,10 +19,6 @@ - - src - tests - diff --git a/src/Feature/SetupUserModelTrait.php b/src/Feature/SetupUserModelTrait.php index 2df8b488..ed150f97 100644 --- a/src/Feature/SetupUserModelTrait.php +++ b/src/Feature/SetupUserModelTrait.php @@ -4,7 +4,7 @@ namespace Atk4\Login\Feature; -// use Atk4\Data\Persistence; // for 5.1.0 compatibility +use Atk4\Data\Persistence; trait SetupUserModelTrait { @@ -21,12 +21,8 @@ public function setupUserModel(): void // all AccessRules for all user roles // @TODO in future when there can be multiple, then merge them together $this->hasMany('AccessRules', [ - // for 5.1.0 compatibility - // 'model' => function (Persistence $p, array $defaults = []) { - // return $this->ref('role_id')->ref('AccessRules'); - // } - 'model' => static function ($m) { - return $m->ref('role_id')->ref('AccessRules'); + 'model' => function (Persistence $p, array $defaults = []) { + return $this->ref('role_id')->ref('AccessRules'); }, 'ourField' => 'role_id', 'theirField' => 'role_id', diff --git a/src/Form/Login.php b/src/Form/Login.php index fb1d9bdb..e43b665a 100644 --- a/src/Form/Login.php +++ b/src/Form/Login.php @@ -36,11 +36,10 @@ protected function init(): void $this->addControl($this->auth->fieldLogin, [], ['required' => true]); - /** @var Form\Control\Password */ - $p = $this->addControl($this->auth->fieldPassword, [Form\Control\Password::class], ['required' => true]); + $pwdControl = $this->addControl($this->auth->fieldPassword, [Form\Control\Password::class], ['required' => true]); if ($this->linkForgot) { - $p->addAction(['icon' => 'question']) + $pwdControl->addAction(['icon' => 'question']) ->setAttr('title', 'Forgot your password?') ->link($this->linkForgot); } diff --git a/src/Form/Register.php b/src/Form/Register.php index d5bd90ba..b0c381c8 100644 --- a/src/Form/Register.php +++ b/src/Form/Register.php @@ -45,13 +45,10 @@ public function setModel(Model $user, array $fields = null): void $this->addControl('name', [], ['required' => true]); $this->addControl('email', [], ['required' => true]); - /** @var Control\Input */ - $p1 = $this->addControl('password', [Control\Password::class], ['type' => 'string', 'required' => true]); - $p1->setInputAttr('autocomplete', 'new-password'); - - /** @var Control\Input */ - $p2 = $this->addControl('password2', [Control\Password::class], ['type' => 'string', 'neverPersist' => true, 'required' => true, 'caption' => 'Repeat Password']); - $p2->setInputAttr('autocomplete', 'new-password'); + $this->addControl('password', [Control\Password::class], ['type' => 'string', 'required' => true]) + ->setInputAttr('autocomplete', 'new-password'); + $this->addControl('password2', [Control\Password::class], ['type' => 'string', 'neverPersist' => true, 'required' => true, 'caption' => 'Repeat Password']) + ->setInputAttr('autocomplete', 'new-password'); // on form submit save new user in persistence $this->onSubmit(function (self $form) { diff --git a/src/Model/AccessRule.php b/src/Model/AccessRule.php index ec88037a..837f9a75 100644 --- a/src/Model/AccessRule.php +++ b/src/Model/AccessRule.php @@ -5,7 +5,6 @@ namespace Atk4\Login\Model; use Atk4\Data\Model; -use Atk4\Data\Reference\HasOneSql; use Atk4\Login\Feature\SetupAccessRuleModelTrait; /** @@ -26,11 +25,9 @@ protected function init(): void { parent::init(); - /** @var HasOneSql */ $r = $this->hasOne('role_id', [ 'model' => $this->roleModelSeed, 'ourField' => 'role_id', - 'theirField' => 'id', 'caption' => 'Role', ]); $r->addTitle(); diff --git a/src/Model/Role.php b/src/Model/Role.php index 358a7bcc..49a899e9 100644 --- a/src/Model/Role.php +++ b/src/Model/Role.php @@ -29,12 +29,10 @@ protected function init(): void $this->hasMany('Users', [ 'model' => $this->userModelSeed, - 'ourField' => 'id', 'theirField' => 'role_id', ]); $this->hasMany('AccessRules', [ 'model' => $this->accessRuleModelSeed, - 'ourField' => 'id', 'theirField' => 'role_id', ]); diff --git a/src/Model/User.php b/src/Model/User.php index c3a75790..afefc2a2 100644 --- a/src/Model/User.php +++ b/src/Model/User.php @@ -6,7 +6,6 @@ use Atk4\Data\Field\PasswordField; use Atk4\Data\Model; -use Atk4\Data\Reference\HasOneSql; use Atk4\Login\Feature\PasswordManagementTrait; use Atk4\Login\Feature\SendEmailActionTrait; use Atk4\Login\Feature\SetupUserModelTrait; @@ -36,11 +35,9 @@ protected function init(): void $this->addField('password', [PasswordField::class, 'ui' => ['form' => [Password::class]]]); // currently user can have only one role. In future it should be n:n relation - /** @var HasOneSql */ $r = $this->hasOne('role_id', [ 'model' => $this->roleModelSeed, 'ourField' => 'role_id', - 'theirField' => 'id', 'caption' => 'Role', ]); $r->addTitle(); diff --git a/src/RoleAdmin.php b/src/RoleAdmin.php index 3bae7b00..87edf8d8 100644 --- a/src/RoleAdmin.php +++ b/src/RoleAdmin.php @@ -28,10 +28,9 @@ public function setModel(Model $role, array $fields = null): void parent::setModel($role); // Add new table column used for actions - /** @var Column\ActionButtons */ - $column = $this->table->addColumn(null, [Column\ActionButtons::class, 'caption' => '']); + $buttons = $this->table->addColumn(null, [Column\ActionButtons::class, 'caption' => '']); - $column->addModal(['icon' => 'cogs'], 'Role Permissions', static function (View $v, $id) use ($role) { + $buttons->addModal(['icon' => 'cogs'], 'Role Permissions', static function (View $v, $id) use ($role) { $role = $role->load($id); Header::addTo($v, [$role->getTitle() . ' Permissions']); diff --git a/src/UserAdmin.php b/src/UserAdmin.php index e3db2c9f..b8c031a3 100644 --- a/src/UserAdmin.php +++ b/src/UserAdmin.php @@ -46,21 +46,19 @@ public function setModel(Model $user): void $this->crud->setModel($user); // Add new table column used for actions - /** @var Column\ActionButtons */ - $column = $this->crud->table->addColumn(null, [Column\ActionButtons::class, 'caption' => '']); + $buttons = $this->crud->table->addColumn(null, [Column\ActionButtons::class, 'caption' => '']); // Pop-up for resetting password. Will display button for generating random password - $column->addModal(['icon' => 'key'], 'Change Password', function (View $v, $id) { + $buttons->addModal(['icon' => 'key'], 'Change Password', function (View $v, $id) { $userEntity = $this->model->load($id); $form = Form::addTo($v); - /** @var Form\Control\Input */ - $f = $form->addControl('visible_password', [], ['required' => true]); + $field = $form->addControl('visible_password', [], ['required' => true]); // $form->addControl('email_user', [], ['type' => 'boolean', 'caption' => 'Email user their new password']); - $f->addAction(['icon' => 'random'])->on('click', static function () use ($f, $userEntity) { - return $f->jsInput()->val(PasswordField::assertInstanceOf($userEntity->getField('password'))->generatePassword()); + $field->addAction(['icon' => 'random'])->on('click', static function () use ($field, $userEntity) { + return $field->jsInput()->val(PasswordField::assertInstanceOf($userEntity->getField('password'))->generatePassword()); }); $form->onSubmit(static function (Form $form) use ($v, $userEntity) { @@ -68,11 +66,8 @@ public function setModel(Model $user): void ->setPassword($userEntity, $form->model->get('visible_password')); $userEntity->save(); - /** @var Modal */ - $modal = $v->getOwner(); - return new JsBlock([ - $modal->jsHide(), + Modal::assertInstanceOf($v->getOwner())->jsHide(), new JsToast([ 'message' => 'Password for ' . $userEntity->get($userEntity->titleField) . ' is changed!', 'class' => 'success',