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 @@
+
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 @@
+