From 706ad256ef104287e07b4aaf4933d9613e7f6067 Mon Sep 17 00:00:00 2001 From: Christian Sciberras Date: Sat, 24 Dec 2022 05:56:52 +0100 Subject: [PATCH] Switch to class builder instead of shims (#1) * Switch to class builder instead of shims * Fix sf6; fix ci * Improve class builder * CI fix for CC * Small improvement * Enable xdebug coverage; small fixes --- .gitattributes | 5 +- .github/workflows/ci.yml | 55 +++++-- .gitignore | 2 +- composer.json | 13 +- phpunit6.xml.dist | 17 +++ phpunit7.xml.dist | 17 +++ phpunit8.xml.dist | 17 +++ phpunit.xml.dist => phpunit9.xml.dist | 0 src/ClassBuilder.php | 140 ++++++++++++++++++ src/ExpressionLanguageWithTplStr.php | 53 ++++--- src/Shims/ExpressionLanguageWithTplStrSF4.php | 30 ---- src/Shims/ExpressionLanguageWithTplStrSF5.php | 30 ---- src/Shims/ExpressionLanguageWithTplStrSF6.php | 31 ---- src/TemplateStringTranslatorTrait.php | 18 ++- 14 files changed, 302 insertions(+), 126 deletions(-) create mode 100644 phpunit6.xml.dist create mode 100644 phpunit7.xml.dist create mode 100644 phpunit8.xml.dist rename phpunit.xml.dist => phpunit9.xml.dist (100%) create mode 100644 src/ClassBuilder.php delete mode 100644 src/Shims/ExpressionLanguageWithTplStrSF4.php delete mode 100644 src/Shims/ExpressionLanguageWithTplStrSF5.php delete mode 100644 src/Shims/ExpressionLanguageWithTplStrSF6.php diff --git a/.gitattributes b/.gitattributes index b509bd5..2abd0b3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,5 +1,8 @@ /.gitattributes export-ignore /.github export-ignore /.gitignore export-ignore -/phpunit.xml.dist export-ignore +/phpunit6.xml.dist export-ignore +/phpunit7.xml.dist export-ignore +/phpunit8.xml.dist export-ignore +/phpunit9.xml.dist export-ignore /tests export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fbaf243..32994bc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,8 @@ name: CI +env: + XDEBUG_MODE: coverage + on: push: branches: @@ -13,27 +16,55 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - php: [ '7.0', '7.1', '7.2', '7.3', '7.4', '8.0' ] + include: + - php: '7.0' + phpunit: '6' + symfony: '3' + - php: '7.1' + phpunit: '7' + symfony: '4' + - php: '7.2' + phpunit: '8' + symfony: '5' + - php: '7.3' + phpunit: '9' + symfony: '5' + - php: '7.4' + phpunit: '9' + symfony: '5' + - php: '8.0' + phpunit: '9' + symfony: '6' steps: - - name: Set up PHP - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - coverage: xdebug2 - - name: Checkout code - uses: actions/checkout@v2 + uses: actions/checkout@v3 with: fetch-depth: 2 - - name: Download dependencies - uses: ramsey/composer-install@v1 + - name: Cache Composer dependencies + uses: actions/cache@v3 with: - composer-options: --no-interaction --prefer-dist --optimize-autoloader + path: /tmp/composer-cache + key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }} + + - name: Set up PHP + uses: php-actions/composer@v6 + with: + php_version: ${{ matrix.php }} + version: 2.2 + php_extensions: xdebug + command: require + only_args: symfony/expression-language:~${{ matrix.symfony }} - name: Run tests - run: ./vendor/bin/phpunit --coverage-clover coverage.xml + uses: php-actions/composer@v6 + with: + php_version: ${{ matrix.php }} + version: 2.2 + php_extensions: xdebug + command: run-tests + only_args: -- --configuration phpunit${{ matrix.phpunit }}.xml.dist - name: Upload to Codecov env: diff --git a/.gitignore b/.gitignore index 779f4cb..a661561 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,4 @@ vendor/ *.cache *.iml composer.lock -phpunit.xml +phpunit*.xml diff --git a/composer.json b/composer.json index e673cc8..a1c2750 100644 --- a/composer.json +++ b/composer.json @@ -5,7 +5,15 @@ "readme": "README.md", "license": "MIT", "description": "Template String support for Symfony Expression Language", - "keywords": ["expression", "language", "dsl", "symfony", "template", "string", "uuf6429"], + "keywords": [ + "expression", + "language", + "dsl", + "symfony", + "template", + "string", + "uuf6429" + ], "authors": [ { "name": "Christian Sciberras", @@ -28,5 +36,8 @@ "psr-4": { "uuf6429\\ExpressionLanguage\\": "tests" } + }, + "scripts": { + "run-tests": "./vendor/bin/phpunit --coverage-clover coverage.xml" } } diff --git a/phpunit6.xml.dist b/phpunit6.xml.dist new file mode 100644 index 0000000..551097b --- /dev/null +++ b/phpunit6.xml.dist @@ -0,0 +1,17 @@ + + + + + ./tests + + + + + ./src + + ./vendor + + + + diff --git a/phpunit7.xml.dist b/phpunit7.xml.dist new file mode 100644 index 0000000..6f9ed15 --- /dev/null +++ b/phpunit7.xml.dist @@ -0,0 +1,17 @@ + + + + + ./tests + + + + + ./src + + ./vendor + + + + diff --git a/phpunit8.xml.dist b/phpunit8.xml.dist new file mode 100644 index 0000000..8417631 --- /dev/null +++ b/phpunit8.xml.dist @@ -0,0 +1,17 @@ + + + + + ./tests + + + + + ./src + + ./vendor + + + + diff --git a/phpunit.xml.dist b/phpunit9.xml.dist similarity index 100% rename from phpunit.xml.dist rename to phpunit9.xml.dist diff --git a/src/ClassBuilder.php b/src/ClassBuilder.php new file mode 100644 index 0000000..9b645ac --- /dev/null +++ b/src/ClassBuilder.php @@ -0,0 +1,140 @@ + + */ + private $overriddenMethods = []; + + public static function create(): ClassBuilder + { + return new self(); + } + + public function class(string $fqn): ClassBuilder + { + $this->child = '\\' . ltrim($fqn, '\\'); + return $this; + } + + public function extend(string $fqn): ClassBuilder + { + $this->parent = '\\' . ltrim($fqn, '\\'); + return $this; + } + + public function use(string $fqn): ClassBuilder + { + $this->usedTraits[] = '\\' . ltrim($fqn, '\\'); + return $this; + } + + public function import(string $fqn): ClassBuilder + { + $this->usedImports[] = '\\' . ltrim($fqn, '\\'); + return $this; + } + + public function override( + string $method, + #[Language('InjectablePHP')] + string $body + ): ClassBuilder { + $this->overriddenMethods[$method] = $body; + return $this; + } + + public function buildClass(): string + { + + list($class, $namespace) = array_map('strrev', explode('\\', strrev($this->child), 2)) + ['']; + + $codeLines = [ + '', + sprintf('namespace %s;', ltrim($namespace, '\\')), + '', + ]; + + foreach ($this->usedImports as $import) { + $codeLines[] = "use $import;"; + } + + $codeLines[] = ''; + + $codeLines[] = "class $class extends $this->parent"; + $codeLines[] = '{'; + + foreach ($this->usedTraits as $trait) { + $codeLines[] = " use $trait;"; + } + + foreach ($this->overriddenMethods as $method => $body) { + if (($sig = $this->extractSignature($this->parent, $method)) !== null) { + $codeLines[] = ''; + array_push($codeLines, ...$sig); + $codeLines[] = ' {'; + $codeLines[] = " $body"; + $codeLines[] = ' }'; + } + } + + $codeLines[] = '}'; + + return implode("\n", $codeLines); + } + + public function createClass() + { + eval($this->buildClass()); + } + + /** + * @param string $class + * @param string $method + * @return string[]|null + */ + private function extractSignature(string $class, string $method) + { + try { + $code = file_get_contents( + (new ReflectionMethod($class, $method))->getFileName() + ); + + return preg_match("/([^}]+function {$method}[^{]+?)\\n\\s+?{/", $code, $matches) + ? array_filter(explode("\n", $matches[1])) + : null; + } catch (ReflectionException $ex) { + return null; + } + } +} diff --git a/src/ExpressionLanguageWithTplStr.php b/src/ExpressionLanguageWithTplStr.php index 43b6ac2..9ee8cac 100644 --- a/src/ExpressionLanguageWithTplStr.php +++ b/src/ExpressionLanguageWithTplStr.php @@ -2,27 +2,42 @@ namespace uuf6429\ExpressionLanguage; -use Throwable; +use Symfony\Component\ExpressionLanguage\Expression as SymfonyExpression; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage as SymfonyExpressionLanguage; +use Symfony\Component\ExpressionLanguage\ParsedExpression as SymfonyParsedExpression; -$instantiable = static function ($class) { - try { - new $class(); - return true; - } catch (Throwable $ex) { - return false; - } -}; +ClassBuilder::create() + ->import(SymfonyExpression::class) + ->import(SymfonyParsedExpression::class) + ->class(ExpressionLanguageWithTplStr::class) + ->extend(SymfonyExpressionLanguage::class) + ->use(TemplateStringTranslatorTrait::class) + ->override( + 'compile', + 'return parent::compile($this->convertExpression($expression), $names);' + ) + ->override( + 'evaluate', + 'return parent::evaluate($this->convertExpression($expression), $values);' + ) + ->override( + 'parse', + 'return parent::parse($this->convertExpression($expression), $names);' + ) + ->override( + 'lint', + 'parent::lint($this->convertExpression($expression), $names);' + ) + ->createClass(); -if ($instantiable(Shims\ExpressionLanguageWithTplStrSF6::class)) { - class ExpressionLanguageWithTplStr extends Shims\ExpressionLanguageWithTplStrSF6 - { - } -} elseif ($instantiable(Shims\ExpressionLanguageWithTplStrSF5::class)) { - class ExpressionLanguageWithTplStr extends Shims\ExpressionLanguageWithTplStrSF5 - { - } -} elseif ($instantiable(Shims\ExpressionLanguageWithTplStrSF4::class)) { - class ExpressionLanguageWithTplStr extends Shims\ExpressionLanguageWithTplStrSF4 +/** + * The class definition below will probably never be executed since the real one is generated by + * the ClassBuilder above. However, the code below is still useful to enable IDE autocompletion. + * @codeCoverageIgnore + */ +if (!class_exists(ExpressionLanguageWithTplStr::class)) { + class ExpressionLanguageWithTplStr extends SymfonyExpressionLanguage { + use TemplateStringTranslatorTrait; } } diff --git a/src/Shims/ExpressionLanguageWithTplStrSF4.php b/src/Shims/ExpressionLanguageWithTplStrSF4.php deleted file mode 100644 index 61a3668..0000000 --- a/src/Shims/ExpressionLanguageWithTplStrSF4.php +++ /dev/null @@ -1,30 +0,0 @@ -translateTplToEl($expression); - } - - return parent::compile($expression, $names); - } - - public function evaluate($expression, $values = []) - { - if (!$expression instanceof ParsedExpression) { - $expression = $this->translateTplToEl($expression); - } - - return parent::evaluate($expression, $values); - } -} diff --git a/src/Shims/ExpressionLanguageWithTplStrSF5.php b/src/Shims/ExpressionLanguageWithTplStrSF5.php deleted file mode 100644 index e7c1079..0000000 --- a/src/Shims/ExpressionLanguageWithTplStrSF5.php +++ /dev/null @@ -1,30 +0,0 @@ -translateTplToEl($expression); - } - - return parent::compile($expression, $names); - } - - public function evaluate($expression, array $values = []) - { - if (!$expression instanceof ParsedExpression) { - $expression = $this->translateTplToEl($expression); - } - - return parent::evaluate($expression, $values); - } -} diff --git a/src/Shims/ExpressionLanguageWithTplStrSF6.php b/src/Shims/ExpressionLanguageWithTplStrSF6.php deleted file mode 100644 index d583b5f..0000000 --- a/src/Shims/ExpressionLanguageWithTplStrSF6.php +++ /dev/null @@ -1,31 +0,0 @@ -translateTplToEl($expression); - } - - return parent::compile($expression, $names); - } - - public function evaluate(Expression|string $expression, array $values = []): mixed - { - if (!$expression instanceof ParsedExpression) { - $expression = $this->translateTplToEl($expression); - } - - return parent::evaluate($expression, $values); - } -} diff --git a/src/TemplateStringTranslatorTrait.php b/src/TemplateStringTranslatorTrait.php index affced8..3121726 100644 --- a/src/TemplateStringTranslatorTrait.php +++ b/src/TemplateStringTranslatorTrait.php @@ -3,6 +3,7 @@ namespace uuf6429\ExpressionLanguage; use SplStack; +use Symfony\Component\ExpressionLanguage\ParsedExpression; use Symfony\Component\ExpressionLanguage\SyntaxError; trait TemplateStringTranslatorTrait @@ -85,9 +86,24 @@ private function translateTplToEl(string $expression): string if ($stateStack->count() !== 1) { $reverse = [$IN_DQS => '"', $IN_SQS => "'", $IN_TPL => '`', $IN_EXP = '}']; - throw new SyntaxError(sprintf('Expected "%s".', $reverse[$stateStack->top()]), strlen($expression), $expression); + throw new SyntaxError( + sprintf('Expected "%s".', $reverse[$stateStack->top()]), + strlen($expression), + $expression + ); } return $result; } + + /** + * @param string|ParsedExpression $expression + * @return string|ParsedExpression + */ + private function convertExpression($expression) + { + return $expression instanceof ParsedExpression + ? $expression + : $this->translateTplToEl($expression); + } }