From a40619eeab9011e37a0902e68fe409b99b1040f0 Mon Sep 17 00:00:00 2001 From: Ezar Affandi Date: Sun, 15 Mar 2026 18:08:36 +0400 Subject: [PATCH] ci: setup PHPUnit, PHPStan, PHP CS Fixer and GitHub Actions workflow --- .gitattributes | 4 + .github/workflows/ci.yaml | 55 +++++++++ .gitignore | 3 + .php-cs-fixer.dist.php | 32 +++++ README.md | 1 + composer.json | 25 ++++ phpstan.neon.dist | 21 ++++ phpunit.xml.dist | 28 +++++ src/Config/Asset.php | 9 +- src/Field/ExtendedTextEditorField.php | 10 +- tests/Unit/Config/AssetTest.php | 68 +++++++++++ .../Field/ExtendedTextEditorFieldTest.php | 113 ++++++++++++++++++ tests/bootstrap.php | 15 +++ 13 files changed, 379 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 .php-cs-fixer.dist.php create mode 100644 phpstan.neon.dist create mode 100644 phpunit.xml.dist create mode 100644 tests/Unit/Config/AssetTest.php create mode 100644 tests/Unit/Field/ExtendedTextEditorFieldTest.php create mode 100644 tests/bootstrap.php diff --git a/.gitattributes b/.gitattributes index 772c653..186c1fc 100644 --- a/.gitattributes +++ b/.gitattributes @@ -12,3 +12,7 @@ /yarn.lock export-ignore /doc export-ignore /.editorconfig export-ignore +/tests export-ignore +/phpunit.xml.dist export-ignore +/phpstan.neon.dist export-ignore +/phpstan-baseline.neon export-ignore diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..94403b9 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,55 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + tests: + name: PHP ${{ matrix.php }} + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + php: ['8.4'] + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + extensions: mbstring, xml, ctype, iconv, intl, pdo_sqlite + coverage: none + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.json') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + run: composer update --prefer-dist --no-progress + + - name: Initialize PHPUnit Bridge + run: vendor/bin/simple-phpunit -v + + - name: Run PHP CS Fixer (Code Style) + if: matrix.php == '8.4' + run: composer cs:check + + - name: Run PHPStan (Static Analysis) + run: composer stan + + - name: Run PHPUnit (Tests) + run: composer tests diff --git a/.gitignore b/.gitignore index 01819f6..47bad3d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,9 @@ composer.lock var vendor/* +.phpunit.result.cache +/phpunit.xml +.php-cs-fixer.cache /node_modules/ npm-debug.log diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000..26756df --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,32 @@ +in(__DIR__ . '/src') + ->in(__DIR__ . '/tests') + ->append([__FILE__]) + ->ignoreDotFiles(true) + ->ignoreVCS(true) + ->files() + ->name('*.php'); + +return new PhpCsFixer\Config() + ->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect()) + ->setRules([ + '@Symfony' => true, + '@Symfony:risky' => true, + '@PHP84Migration' => true, + 'declare_strict_types' => true, + 'global_namespace_import' => [ + 'import_classes' => true, + 'import_constants' => true, + 'import_functions' => true, + ], + 'yoda_style' => false, // Disable the Yoda style (e.g., if (null === $var)) if you prefer the classic style (if ($var === null)). + 'concat_space' => ['spacing' => 'one'], // Adds a space around the concatenation points + ]) + ->setUsingCache(true) + ->setRiskyAllowed(true) + ->setFinder($finder) + ->setCacheFile(__DIR__ . '/var/cache/.php-cs-fixer.cache'); // Put the cache away neatly. diff --git a/README.md b/README.md index 9e48538..dacc5d7 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ Latest Stable Version Total Downloads License + CI

This Symfony bundle extends the native capabilities of EasyAdmin's `TextEditorField` (based on Trix). It offers an enhanced writing experience by adding essential features that are missing by default, all without any complex front-end configuration. diff --git a/composer.json b/composer.json index 522f342..3472b41 100644 --- a/composer.json +++ b/composer.json @@ -25,17 +25,42 @@ "symfony/framework-bundle": "^6.0|^7.0|^8.0", "easycorp/easyadmin-bundle": "^4.0|^5.0" }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.94", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "symfony/phpunit-bridge": "^6.4.26|^7.0|^8.0" + }, "autoload": { "psr-4": { "Ezar101\\EasyAdminTrixExtensionBundle\\": "src/" } }, + "autoload-dev": { + "psr-4": { + "Ezar101\\EasyAdminTrixExtensionBundle\\Tests\\": "tests/" + } + }, "config": { "sort-packages": true, "allow-plugins": { + "phpstan/extension-installer": true, "symfony/flex": true, "symfony/runtime": false } }, + "scripts": { + "tests": "simple-phpunit", + "cs:check": "php-cs-fixer fix --dry-run --diff", + "cs:fix": "php-cs-fixer fix", + "stan": "phpstan analyse", + "qa": [ + "@cs:check", + "@stan", + "@tests" + ] + }, "minimum-stability": "stable" } diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..bcc197d --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,21 @@ +parameters: + level: 6 + phpVersion: 80400 + paths: + - src/ + - tests/ + + excludePaths: + - vendor/ + + bootstrapFiles: + - vendor/bin/.phpunit/phpunit/vendor/autoload.php + + ignoreErrors: + - + identifier: missingType.generics + + treatPhpDocTypesAsCertain: false + reportUnmatchedIgnoredErrors: false + + tmpDir: var/cache/phpstan diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..247faa7 --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + tests/ + + + + + + src + + + diff --git a/src/Config/Asset.php b/src/Config/Asset.php index e636250..2f7a4e5 100644 --- a/src/Config/Asset.php +++ b/src/Config/Asset.php @@ -4,8 +4,13 @@ namespace Ezar101\EasyAdminTrixExtensionBundle\Config; +use JsonException; + +use const JSON_THROW_ON_ERROR; + final class Asset { + /** @var array|null */ private static ?array $manifest = null; private function __construct() @@ -20,8 +25,8 @@ public static function from(string $filename): string if (file_exists($path)) { try { $content = file_get_contents($path); - self::$manifest = json_decode($content, true, 512, JSON_THROW_ON_ERROR) ?: []; - } catch (\JsonException $e) { + self::$manifest = json_decode($content, true, 512, JSON_THROW_ON_ERROR) ?? []; + } catch (JsonException $e) { self::$manifest = []; } } else { diff --git a/src/Field/ExtendedTextEditorField.php b/src/Field/ExtendedTextEditorField.php index b80fb21..02ac930 100644 --- a/src/Field/ExtendedTextEditorField.php +++ b/src/Field/ExtendedTextEditorField.php @@ -4,13 +4,16 @@ namespace Ezar101\EasyAdminTrixExtensionBundle\Field; +use EasyCorp\Bundle\EasyAdminBundle\Config\Asset as EasyAdminAsset; use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface; use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait; use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField; use EasyCorp\Bundle\EasyAdminBundle\Form\Type\TextEditorType; use Ezar101\EasyAdminTrixExtensionBundle\Config\Asset; +use InvalidArgumentException; use Symfony\Contracts\Translation\TranslatableInterface; -use EasyCorp\Bundle\EasyAdminBundle\Config\Asset as EasyAdminAsset; + +use function sprintf; final class ExtendedTextEditorField implements FieldInterface { @@ -18,7 +21,7 @@ final class ExtendedTextEditorField implements FieldInterface public static function new(string $propertyName, TranslatableInterface|bool|string|null $label = null): self { - return (new self) + return new self() ->setProperty($propertyName) ->setLabel($label) ->setTemplateName('crud/field/text_editor') @@ -41,7 +44,7 @@ public static function new(string $propertyName, TranslatableInterface|bool|stri public function setNumOfRows(int $rows): self { if ($rows < 1) { - throw new \InvalidArgumentException(sprintf('The argument of the "%s()" method must be 1 or higher (%d given).', __METHOD__, $rows)); + throw new InvalidArgumentException(sprintf('The argument of the "%s()" method must be 1 or higher (%d given).', __METHOD__, $rows)); } $this->setCustomOption(TextEditorField::OPTION_NUM_OF_ROWS, $rows); @@ -51,6 +54,7 @@ public function setNumOfRows(int $rows): self /** * @param array $config + * * @see TextEditorField::setTrixEditorConfig() */ public function setTrixEditorConfig(array $config): self diff --git a/tests/Unit/Config/AssetTest.php b/tests/Unit/Config/AssetTest.php new file mode 100644 index 0000000..3cbc5c7 --- /dev/null +++ b/tests/Unit/Config/AssetTest.php @@ -0,0 +1,68 @@ +setStaticPropertyValue('manifest', null); + + parent::tearDown(); + } + + public function testAssetReturnsCleanPathWhenManifestDoesNotContainFile(): void + { + // We simulate a manifest that has been loaded but does not contain the file, + // (or the case where the file did not exist and returned an empty array). + // By using an empty array [], we prevent the class from reading the actual file on disk. + $reflection = new ReflectionClass(Asset::class); + $reflection->setStaticPropertyValue('manifest', []); + + $result = Asset::from('extended-trix.css'); + + self::assertSame('bundles/easyadmintrixextension/extended-trix.css', $result); + } + + public function testAssetUsesManifestWhenAvailable(): void + { + // We simulate a loaded manifest by directly injecting an array + // via Reflection, without having to create a fake physical file. + $manifestMock = [ + 'extended-trix.css' => 'extended-trix.8b7c6d5e.css', + 'extended-trix.js' => 'extended-trix.1a2b3c4d.js', + ]; + + $reflection = new ReflectionClass(Asset::class); + $reflection->setStaticPropertyValue('manifest', $manifestMock); + + $resultCss = Asset::from('extended-trix.css'); + $resultJs = Asset::from('extended-trix.js'); + + self::assertSame('bundles/easyadmintrixextension/extended-trix.8b7c6d5e.css', $resultCss); + self::assertSame('bundles/easyadmintrixextension/extended-trix.1a2b3c4d.js', $resultJs); + } + + public function testAssetStripsLeadingDotSlash(): void + { + // Sometimes Webpack Encore generates manifests with "./file.css" + // We check that ltrim($target, './') works correctly. + $reflection = new ReflectionClass(Asset::class); + $reflection->setStaticPropertyValue('manifest', [ + 'app.css' => './assets/app.1234.css', + ]); + + $result = Asset::from('app.css'); + + self::assertSame('bundles/easyadmintrixextension/assets/app.1234.css', $result); + } +} diff --git a/tests/Unit/Field/ExtendedTextEditorFieldTest.php b/tests/Unit/Field/ExtendedTextEditorFieldTest.php new file mode 100644 index 0000000..6914fad --- /dev/null +++ b/tests/Unit/Field/ExtendedTextEditorFieldTest.php @@ -0,0 +1,113 @@ +getAsDto(); + + self::assertSame('content', $dto->getProperty()); + self::assertSame('Post Content', $dto->getLabel()); + self::assertSame('crud/field/text_editor', $dto->getTemplateName()); + self::assertSame(TextEditorType::class, $dto->getFormType()); + self::assertSame('field-text_editor', $dto->getCssClass()); + self::assertSame('col-md-9 col-xxl-7', $dto->getColumns()); + } + + public function testFieldAssetsAreInjected(): void + { + $field = ExtendedTextEditorField::new('content'); + $dto = $field->getAsDto(); + $assets = $dto->getAssets(); + + $cssAssets = $assets->getCssAssets(); + /* + * The value of $cssAssets looks like this: + * [ + * "field-text-editor.css" => EasyCorp\Bundle\EasyAdminBundle\Dto\AssetDto { + * -value: "field-text-editor.css" + * } + * "bundles/easyadmintrixextension/extended-trix.xxxx.css" => EasyCorp\Bundle\EasyAdminBundle\Dto\AssetDto { + * -value: "bundles/easyadmintrixextension/extended-trix.xxxx.css" + * } + * ] + */ + + self::assertCount(2, $cssAssets); + self::assertMatchesRegularExpression( + '/extended-trix.*\.css$/', + end($cssAssets)->getValue(), + ); + + $jsAssets = $assets->getJsAssets(); + /* + * The value of $jsAssets looks like this: + * [ + * "field-text-editor.js" => EasyCorp\Bundle\EasyAdminBundle\Dto\AssetDto { + * -value: "field-text-editor.js" + * } + * "bundles/easyadmintrixextension/extended-trix.xxxx.js" => EasyCorp\Bundle\EasyAdminBundle\Dto\AssetDto { + * -value: "bundles/easyadmintrixextension/extended-trix.xxxx.js" + * } + * ] + */ + + self::assertCount(2, $jsAssets); + self::assertMatchesRegularExpression( + '/extended-trix.*\.js$/', + end($jsAssets)->getValue(), + ); + } + + public function testDefaultTrixConfigIsSet(): void + { + $field = ExtendedTextEditorField::new('content'); + $dto = $field->getAsDto(); + $config = $dto->getCustomOption(TextEditorField::OPTION_TRIX_EDITOR_CONFIG); + + self::assertIsArray($config); + self::assertArrayHasKey('blockAttributes', $config); + self::assertArrayHasKey('heading2', $config['blockAttributes']); + self::assertArrayHasKey('heading3', $config['blockAttributes']); + + self::assertArrayHasKey('textAttributes', $config); + self::assertArrayHasKey('color', $config['textAttributes']); + self::assertArrayHasKey('underline', $config['textAttributes']); + } + + public function testSetNumOfRowsThrowsExceptionForInvalidValue(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The argument of the "Ezar101\EasyAdminTrixExtensionBundle\Field\ExtendedTextEditorField::setNumOfRows()" method must be 1 or higher (0 given).'); + + ExtendedTextEditorField::new('content')->setNumOfRows(0); + } + + public function testSetNumOfRowsUpdatesCustomOption(): void + { + $field = ExtendedTextEditorField::new('content')->setNumOfRows(10); + $dto = $field->getAsDto(); + + self::assertSame(10, $dto->getCustomOption(TextEditorField::OPTION_NUM_OF_ROWS)); + } + + public function testSetTrixEditorConfigOverridesDefaultConfig(): void + { + $customConfig = ['foo' => 'bar']; + $field = ExtendedTextEditorField::new('content')->setTrixEditorConfig($customConfig); + $dto = $field->getAsDto(); + + self::assertSame($customConfig, $dto->getCustomOption(TextEditorField::OPTION_TRIX_EDITOR_CONFIG)); + } +} diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..91bf235 --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,15 @@ +