diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 27d9a2e..03fb02d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,3 +92,25 @@ jobs: - name: Run JavaScript unit and browser tests run: npm test + + phpstan: + name: PHPStan + runs-on: ubuntu-22.04 + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + coverage: none + tools: composer:v2 + + - name: Install Composer dependencies + run: composer update --no-interaction --prefer-dist --no-progress + + - name: Run PHPStan + run: composer phpstan diff --git a/AGENTS.md b/AGENTS.md index 4310404..95fdc4e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,8 @@ Local path: `/Volumes/SRC/JsFormValidatorBundle` - PR #173 revived the project and was merged into `formapro/master`. - PR #174 restored GitHub Actions CI and was merged into `formapro/master`. - PR #175 restored selected old PR fixes and was merged into `formapro/master`. -- Replacement PRs #176, #177, and #178 are open, ready for review, and were last checked as `MERGEABLE / CLEAN` with green CI. +- Replacement PRs #176, #177, and #178 were merged into `formapro/master`. +- `1.7.0-beta1` was published from `formapro/master`. ## CI @@ -18,16 +19,17 @@ Local path: `/Volumes/SRC/JsFormValidatorBundle` - The PHP job runs `composer update`, `composer validate --strict`, and `composer test`. - The JavaScript job runs on Node `22` with PHP `8.3`. - The JavaScript job installs Cypress system dependencies, then runs `composer update`, `npm install`, and `npm test`. +- The PHPStan job runs on PHP `8.3`, installs dependencies with `composer update`, warms the Symfony test cache, and runs `composer phpstan`. - The old `.travis.yml` file was removed. - `README.md` was updated to use a GitHub Actions badge and test instructions instead of Travis CI references. - `package.json` no longer advertises the old Travis CI badge in the package description. ## Local Validation -- `php /tmp/jsfv-composer.phar test` passes: `3 tests, 14 assertions`. -- `npm test` passes: Jest `184 tests`; Cypress e2e `16 tests`. +- `php /tmp/jsfv-composer.phar test` passes: `5 tests, 18 assertions`. +- `php /tmp/jsfv-composer.phar phpstan` runs PHPStan with `phpstan.neon`. +- `npm test` passes: Jest `197 tests`; Cypress e2e `16 tests`. - The local `composer` shim is broken with `Could not open input file: /Users/ton/bin/composer`, so use `/tmp/jsfv-composer.phar` locally if needed. -- The working tree was clean after the latest validation. ## GitHub Checks Note diff --git a/README.md b/README.md index ee4bb29..efc30a4 100644 --- a/README.md +++ b/README.md @@ -192,6 +192,12 @@ Run the PHP test suite: composer test ``` +Run PHPStan static analysis: + +```bash +composer phpstan +``` + Run the JavaScript unit tests and Cypress browser smoke test: ```bash @@ -205,5 +211,5 @@ composer validate --strict git diff --check ``` -The same maintained test contour is also run by GitHub Actions on pushes and -pull requests. +The same maintained test and static-analysis checks are also run by GitHub +Actions on pushes and pull requests. diff --git a/Tests/Controller/AjaxControllerTest.php b/Tests/Controller/AjaxControllerTest.php index 296ef0f..f9d526f 100644 --- a/Tests/Controller/AjaxControllerTest.php +++ b/Tests/Controller/AjaxControllerTest.php @@ -104,6 +104,10 @@ private function createRegistry(ObjectRepository $repository) } } +class InMemoryEntity +{ +} + class InMemoryRepository implements ObjectRepository { public function find(mixed $id): ?object @@ -134,8 +138,11 @@ public function findOneBy(array $criteria): ?object return null; } + /** + * @return class-string + */ public function getClassName(): string { - return 'InMemoryEntity'; + return InMemoryEntity::class; } } diff --git a/composer.json b/composer.json index 3141bf3..f9fa8c5 100644 --- a/composer.json +++ b/composer.json @@ -33,6 +33,8 @@ }, "require-dev": { + "phpstan/phpstan": "^2.1", + "phpstan/phpstan-symfony": "^2.0", "phpunit/phpunit": "^10.5 || ^11.5", "symfony/asset": "^5.4 || ^6.4 || ^7.0", "symfony/console": "^5.4 || ^6.4 || ^7.0", @@ -76,6 +78,10 @@ }, "scripts": { + "phpstan": [ + "APP_ENV=test APP_DEBUG=1 php Tests/app/bin/console cache:warmup --env=test", + "phpstan analyse --memory-limit=1G" + ], "test": "phpunit" } } diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..58aaf2f --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,13 @@ +includes: + - vendor/phpstan/phpstan-symfony/extension.neon + - vendor/phpstan/phpstan-symfony/rules.neon + +parameters: + level: 5 + paths: + - src + - Tests/Controller + - Tests/Unit + - Tests/app/src + symfony: + containerXmlPath: Tests/app/var/cache/test/App_KernelTestDebugContainer.xml diff --git a/src/Factory/JsFormValidatorFactory.php b/src/Factory/JsFormValidatorFactory.php index 344ffd7..48d345e 100755 --- a/src/Factory/JsFormValidatorFactory.php +++ b/src/Factory/JsFormValidatorFactory.php @@ -12,9 +12,9 @@ use Symfony\Component\Form\FormInterface; use Symfony\Contracts\Translation\TranslatorInterface; use Symfony\Component\Validator\Constraint; -use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\ClassMetadataInterface; use Symfony\Component\Validator\Mapping\GetterMetadata; -use Symfony\Component\Validator\Mapping\PropertyMetadata; +use Symfony\Component\Validator\Mapping\MetadataInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; /** @@ -87,7 +87,7 @@ public function __construct( * * @param string $className * - * @return ClassMetadata + * @return MetadataInterface * @codeCoverageIgnore */ protected function getMetadataFor($className) @@ -170,7 +170,7 @@ public function getQueue() * * @param FormInterface $form * - * @return array + * @return void */ public function addToQueue(FormInterface $form) { @@ -316,15 +316,16 @@ protected function getValidationData(FormInterface $form) // If parent has metadata $parent = $form->getParent(); if ($parent && null !== $parent->getConfig()->getDataClass()) { - $classMetadata = $metadata = $this->getMetadataFor($parent->getConfig()->getDataClass()); - if ($classMetadata->hasPropertyMetadata($form->getName())) { + $classMetadata = $this->getMetadataFor($parent->getConfig()->getDataClass()); + if ($classMetadata instanceof ClassMetadataInterface && $classMetadata->hasPropertyMetadata($form->getName())) { $metadata = $classMetadata->getPropertyMetadata($form->getName()); - /** @var PropertyMetadata $item */ foreach ($metadata as $item) { + $constraints = $item instanceof GetterMetadata ? array() : $item->getConstraints(); + $getters = $item instanceof GetterMetadata ? array($item) : array(); $this->composeValidationData( $parentData, - $item->getConstraints(), - $getters = !empty($item->getters) ? (array)$item->getters : array() + $constraints, + $getters ); } } @@ -335,7 +336,7 @@ protected function getValidationData(FormInterface $form) $this->composeValidationData( $ownData, $metadata->getConstraints(), - $getters = !empty($metadata->getters) ? (array)$metadata->getters : array() + $this->getGetterMetadata($metadata) ); } // If has constraints in a form element @@ -364,6 +365,27 @@ protected function getValidationData(FormInterface $form) return $result; } + /** + * @return GetterMetadata[] + */ + protected function getGetterMetadata(MetadataInterface $metadata) + { + if (!$metadata instanceof ClassMetadataInterface) { + return array(); + } + + $getters = array(); + foreach ($metadata->getConstrainedProperties() as $property) { + foreach ($metadata->getPropertyMetadata($property) as $item) { + if ($item instanceof GetterMetadata) { + $getters[] = $item; + } + } + } + + return $getters; + } + protected function mergeDataRecursive(array $array1, array $array2) { foreach ($array2 as $key => $value) { @@ -515,7 +537,7 @@ protected function getTransformerParam(DataTransformerInterface $transformer, $p $reflection = new \ReflectionProperty($transformer, $paramName); $reflection->setAccessible(true); - if (method_exists($reflection, 'isInitialized') && !$reflection->isInitialized($transformer)) { + if (!$reflection->isInitialized($transformer)) { return null; } diff --git a/src/Form/Constraint/UniqueEntity.php b/src/Form/Constraint/UniqueEntity.php index f97b049..46280e1 100755 --- a/src/Form/Constraint/UniqueEntity.php +++ b/src/Form/Constraint/UniqueEntity.php @@ -31,6 +31,10 @@ class UniqueEntity extends BaseUniqueEntity */ public function __construct(BaseUniqueEntity $base, $entityName, $entity = null) { + foreach (get_object_vars($base) as $prop => $value) { + $this->{$prop} = $value; + } + $this->entityName = $entityName; if (is_object($entity)) { $this->entity = $entity; @@ -38,9 +42,5 @@ public function __construct(BaseUniqueEntity $base, $entityName, $entity = null) $this->entityId = $entity->getId(); } } - - foreach ($base as $prop => $value) { - $this->{$prop} = $value; - } } } diff --git a/src/Model/JsModelAbstract.php b/src/Model/JsModelAbstract.php index cfad45e..462a909 100644 --- a/src/Model/JsModelAbstract.php +++ b/src/Model/JsModelAbstract.php @@ -49,7 +49,8 @@ public static function phpValueToJs($value) // For an object or associative array elseif (is_object($value) || (is_array($value) && array_values($value) !== $value)) { $jsObject = array(); - foreach ($value as $paramName => $paramValue) { + $properties = is_object($value) ? get_object_vars($value) : $value; + foreach ($properties as $paramName => $paramValue) { $paramName = addcslashes($paramName, '\'\\'); $jsObject[] = "'$paramName':" . self::phpValueToJs($paramValue); } @@ -77,7 +78,7 @@ public static function phpValueToJs($value) } // For numbers elseif (is_numeric($value)) { - return $value; + return (string) $value; } // For null elseif (is_null($value)) { @@ -95,7 +96,7 @@ public static function phpValueToJs($value) public function toArray() { $result = array(); - foreach ($this as $key => $value) { + foreach (get_object_vars($this) as $key => $value) { $result[$key] = $value; }