From 9fc23fbf2487300a98a3b7d22b02f0c8d5fe5e1e Mon Sep 17 00:00:00 2001 From: korridor <26689068+korridor@users.noreply.github.com> Date: Fri, 25 Sep 2020 15:47:08 +0200 Subject: [PATCH 1/9] Added validation command; Renamed command; Fixed bug in calculate command; Added tests; Added TravisCI, codecoverage and StyleCI --- .gitignore | 4 + .php_cs | 14 ++ .styleci.yml | 1 + .travis.yml | 83 ++++++++++++ composer.json | 26 +++- config/computed-attributes.php | 6 + docker/Dockerfile | 24 ++++ docker/docker-compose.yml | 9 ++ phpcs.xml | 9 ++ phpunit.xml | 38 ++++++ readme.md | 39 +++++- src/Console/GenerateComputedAttributes.php | 125 ++++++------------ src/Console/ValidateComputedAttributes.php | 111 ++++++++++++++++ ...ravelComputedAttributesServiceProvider.php | 14 +- src/Parser/ModelAttributeParser.php | 106 +++++++++++++++ src/Parser/ModelAttributesEntry.php | 44 ++++++ src/Parser/ParsingException.php | 10 ++ .../GenerateComputedAttributesCommandTest.php | 88 ++++++++++++ tests/TestCase.php | 33 +++++ .../0000_00_00_000000_add_posts_table.php | 36 +++++ .../0000_00_00_000001_add_votes_table.php | 39 ++++++ tests/{ => TestEnvironment}/Models/Post.php | 26 +++- tests/TestEnvironment/Models/Vote.php | 35 +++++ 23 files changed, 827 insertions(+), 93 deletions(-) create mode 100644 .php_cs create mode 100644 .styleci.yml create mode 100644 .travis.yml create mode 100644 config/computed-attributes.php create mode 100644 docker/Dockerfile create mode 100644 docker/docker-compose.yml create mode 100644 phpcs.xml create mode 100644 phpunit.xml create mode 100644 src/Console/ValidateComputedAttributes.php create mode 100644 src/Parser/ModelAttributeParser.php create mode 100644 src/Parser/ModelAttributesEntry.php create mode 100644 src/Parser/ParsingException.php create mode 100644 tests/Feature/GenerateComputedAttributesCommandTest.php create mode 100644 tests/TestCase.php create mode 100644 tests/TestEnvironment/Migrations/0000_00_00_000000_add_posts_table.php create mode 100644 tests/TestEnvironment/Migrations/0000_00_00_000001_add_votes_table.php rename tests/{ => TestEnvironment}/Models/Post.php (52%) create mode 100644 tests/TestEnvironment/Models/Vote.php diff --git a/.gitignore b/.gitignore index 2ab3bab..04db3f9 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,7 @@ .Trashes ehthumbs.db Thumbs.db +vendor +composer.lock +.php_cs.cache +.phpunit.result.cache diff --git a/.php_cs b/.php_cs new file mode 100644 index 0000000..398a1a5 --- /dev/null +++ b/.php_cs @@ -0,0 +1,14 @@ +setRiskyAllowed(false) + ->setRules([ + '@PSR2' => true, + ]) + ->setUsingCache(true) + ->setFinder( + PhpCsFixer\Finder::create() + ->in(__DIR__.'/src') + ->in(__DIR__.'/tests') + ) +; diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..0285f17 --- /dev/null +++ b/.styleci.yml @@ -0,0 +1 @@ +preset: laravel diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..8a64a41 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,83 @@ +cache: + directories: + - $HOME/.composer/cache + +language: php + +matrix: + include: + # Laravel 5.6.* + - php: 7.1 + env: LARAVEL='5.6.*' TESTBENCH='3.6.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.2 + env: LARAVEL='5.6.*' TESTBENCH='3.6.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.3 + env: LARAVEL='5.6.*' TESTBENCH='3.6.*' COMPOSER_FLAGS='--prefer-stable' + # Laravel 5.7.* + - php: 7.1 + env: LARAVEL='5.7.*' TESTBENCH='3.7.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.2 + env: LARAVEL='5.7.*' TESTBENCH='3.7.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.3 + env: LARAVEL='5.7.*' TESTBENCH='3.7.*' COMPOSER_FLAGS='--prefer-stable' + # Laravel 5.8.* + - php: 7.1 + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.1 + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.2 + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.2 + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.3 + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.3 + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' + # Laravel 6.* + - php: 7.2 + env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.2 + env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.3 + env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.3 + env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-stable' + # Laravel 7.* + - php: 7.2 + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.2 + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.3 + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.3 + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.4 + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.4 + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-stable' + # Laravel 8.* + - php: 7.3 + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.3 + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-stable' + - php: 7.4 + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-lowest' + - php: 7.4 + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-stable' + fast_finish: true + +before_install: + - travis_retry composer self-update + - travis_retry composer require "laravel/framework:${LARAVEL}" "orchestra/testbench:${TESTBENCH}" --no-interaction --no-update + +install: + - travis_retry composer update ${COMPOSER_FLAGS} --prefer-dist --no-interaction --no-suggest + +before_script: + - composer config discard-changes true + +script: + - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/composer.json b/composer.json index e2d66d7..3332240 100644 --- a/composer.json +++ b/composer.json @@ -12,16 +12,34 @@ ], "minimum-stability": "stable", "require": { - "php": "^7.1|^7.2|^7.3|^7.4", - "illuminate/support": "^5.6|^5.7|^5.8|^6|^7", - "illuminate/database": "^5.6|^5.7|^5.8|^6|^7", - "illuminate/console": "^5.6|^5.7|^5.8|^6|^7" + "php": "^7.1", + "composer/composer": "^1.10", + "illuminate/console": "^5.6|^6|^7|^8", + "illuminate/database": "^5.6|^6|^7|^8", + "illuminate/support": "^5.6|^6|^7|^8" + }, + "require-dev": { + "orchestra/testbench": "^3.6|^4.0|^5.0|^6.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0", + "friendsofphp/php-cs-fixer": "^2.16", + "squizlabs/php_codesniffer": "^3.5" }, "autoload": { "psr-4": { "Korridor\\LaravelComputedAttributes\\": "src" } }, + "autoload-dev": { + "psr-4": { + "Korridor\\LaravelComputedAttributes\\Tests\\": "tests/" + } + }, + "scripts": { + "test": "vendor/bin/phpunit", + "test-coverage": "vendor/bin/phpunit --coverage-html coverage", + "fix": "./vendor/bin/php-cs-fixer fix", + "lint": "./vendor/bin/phpcs --error-severity=1 --warning-severity=8 --extensions=php" + }, "extra": { "laravel": { "providers": [ diff --git a/config/computed-attributes.php b/config/computed-attributes.php new file mode 100644 index 0000000..d5e1732 --- /dev/null +++ b/config/computed-attributes.php @@ -0,0 +1,6 @@ + 'app/Models', + 'model_namespace' => 'App\\Models', +]; diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..7dc8b96 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,24 @@ +FROM php:7.4-cli + +RUN apt-get update && apt-get install -y \ + zlib1g-dev \ + libzip-dev + +RUN docker-php-ext-install zip + +RUN pecl install xdebug-2.8.1 \ + && docker-php-ext-enable xdebug + +# Install composer and add its bin to the PATH. +RUN curl -s http://getcomposer.org/installer | php && \ + echo "export PATH=${PATH}:/var/www/vendor/bin" >> ~/.bashrc && \ + mv composer.phar /usr/local/bin/composer + +# Add bash aliases +RUN echo "alias ll='ls --color=auto -al'" >> ~/.bashrc + + +# Source the bash +RUN . ~/.bashrc + +WORKDIR /usr/src/app diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..dd54d6e --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,9 @@ +version: '3.7' +services: + workspace: + build: + context: . + volumes: + - ..:/usr/src/app + tty: true + stdin_open: true diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..4f5006d --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,9 @@ + + + The ASH2 coding standard. + + + + src/ + tests/ + diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..e312d86 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,38 @@ + + + + + src/ + + + + + tests/Feature + + + + + + + + + + + + + + + + + diff --git a/readme.md b/readme.md index e4fa174..4a64d23 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,10 @@ # Laravel computed attributes -Warning: This package is still under heavy development. +[![Latest Version on Packagist](https://img.shields.io/packagist/v/korridor/laravel-computed-attributes?style=flat-square)](https://packagist.org/packages/korridor/laravel-computed-attributes) +[![License](https://img.shields.io/packagist/l/korridor/laravel-computed-attributes?style=flat-square)](license.md) +[![TravisCI](https://img.shields.io/travis/korridor/laravel-computed-attributes?style=flat-square)](https://travis-ci.org/korridor/laravel-computed-attributes) +[![Codecov](https://img.shields.io/codecov/c/github/korridor/laravel-computed-attributes?style=flat-square)](https://codecov.io/gh/korridor/laravel-computed-attributes) +[![StyleCI](https://styleci.io/repos/226346821/shield)](https://styleci.io/repos/226346821) ## Installation @@ -10,6 +14,39 @@ You can install the package via composer with following command: composer require korridor/laravel-computed-attributes ``` +### Requirements + +This package is tested for the following Laravel versions: + + - 8.* + - 7.* + - 6.* + - 5.8.* + - 5.7.* (stable only) + - 5.6.* (stable only) + +## Usage examples + + + +## Contributing + +I am open for suggestions and contributions. Just create an issue or a pull request. + +### Testing + +```bash +composer test +composer test-coverage +``` + +### Codeformatting/Linting + +```bash +composer fix +composer lint +``` + ## License This package is licensed under the MIT License (MIT). Please see [license file](license.md) for more information. diff --git a/src/Console/GenerateComputedAttributes.php b/src/Console/GenerateComputedAttributes.php index c57ddc5..1ea9db3 100644 --- a/src/Console/GenerateComputedAttributes.php +++ b/src/Console/GenerateComputedAttributes.php @@ -2,11 +2,14 @@ namespace Korridor\LaravelComputedAttributes\Console; -use Composer\Autoload\ClassMapGenerator; use Illuminate\Console\Command; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Log; use Korridor\LaravelComputedAttributes\ComputedAttributes; -use ReflectionClass; +use Korridor\LaravelComputedAttributes\Parser\ModelAttributeParser; +use Korridor\LaravelComputedAttributes\Parser\ParsingException; +use Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post; use ReflectionException; /** @@ -19,9 +22,9 @@ class GenerateComputedAttributes extends Command * * @var string */ - protected $signature = 'computed-attributes:generate '. - '{modelsAttributes? : List of models and optionally their attributes (example: "FullModel;PartModel:attribute_1,attribute_2" or "OtherNamespace\OtherModel")} '. - '{--chunkSize=100 : Size of the model chunk}'; + protected $signature = 'computed-attributes:generate ' . + '{modelsAttributes? : List of models and optionally their attributes (example: "FullModel;PartModel:attribute_1,attribute_2" or "OtherNamespace\OtherModel")} ' . + '{--chunkSize= : Size of the model chunk}'; /** * The console command description. @@ -41,104 +44,55 @@ public function __construct() /** * Execute the console command. * - * @return bool + * @return int * @throws ReflectionException */ public function handle() { $modelsWithAttributes = $this->argument('modelsAttributes'); - $modelPath = app_path('Models'); - $modelNamespace = 'App\\Models\\'; - $chunkSizeRaw = $this->option('chunkSize'); - if (preg_match('/^\d+$/', $chunkSizeRaw)) { - $chunkSize = intval($chunkSizeRaw); - if ($chunkSize < 1) { - $this->error('Option chunkSize needs to be greater than zero'); - return false; - } - } else { - $this->error('Option chunkSize needs to be an integer'); + $this->info('Parsing arguments...'); - return false; - } + // Validate and parse modelsAttributes argument + $modelAttributeParser = app(ModelAttributeParser::class); + try { + $modelAttributesEntries = $modelAttributeParser->getModelAttributeEntries($modelsWithAttributes); + } catch (ParsingException $exception) { + var_dump($exception->getMessage()); + $this->error($exception->getMessage()); - // Get all models with trait - $classmap = ClassMapGenerator::createMap($modelPath); - $models = []; - foreach ($classmap as $class => $filepath) { - $reflection = new ReflectionClass($class); - $traits = $reflection->getTraitNames(); - foreach ($traits as $trait) { - if ('Korridor\\LaravelComputedAttributes\\ComputedAttributes' === $trait) { - array_push($models, $class); - } - } + return 1; } - // Get all class/attribute combinations - $modelAttributesToProcess = []; - if (null === $modelsWithAttributes) { - $this->info('Start calculating for all models with trait...'); - foreach ($models as $model) { - /** @var Model|ComputedAttributes $modelInstance */ - $modelInstance = new $model(); - $attributes = $modelInstance->getComputedAttributeConfiguration(); - array_push($modelAttributesToProcess, [ - 'model' => $model, - 'modelInstance' => $modelInstance, - 'attributes' => $attributes, - ]); - } - } else { - $this->info('Start calculating for given models...'); - $modelsInAttribute = explode(';', $modelsWithAttributes); - foreach ($modelsInAttribute as $modelInAttribute) { - $modelInAttributeExploded = explode(':', $modelInAttribute); - if (1 !== sizeof($modelInAttributeExploded) && 2 !== sizeof($modelInAttributeExploded)) { - $this->error('Parsing error'); - - return false; - } - $model = $modelNamespace.$modelInAttributeExploded[0]; - if (in_array($model, $models)) { - /** @var Model|ComputedAttributes $modelInstance */ - $modelInstance = new $model(); - } else { - $this->error('Model "'.$model.'" not found'); + // Validate chunkSize option + $chunkSizeRaw = $this->option('chunkSize'); + if ($chunkSizeRaw !== null) { + if (preg_match('/^\d+$/', $chunkSizeRaw)) { + $chunkSize = intval($chunkSizeRaw); + if ($chunkSize < 1) { + $this->error('Option chunkSize needs to be greater than zero'); - return false; + return 1; } - $attributes = $modelInstance->getComputedAttributeConfiguration(); - if (2 === sizeof($modelInAttributeExploded)) { - $attributeWhitelistItems = explode(',', $modelInAttributeExploded[1]); - foreach ($attributeWhitelistItems as $attributeWhitelistItem) { - if (in_array($attributeWhitelistItem, $attributes)) { - } else { - $this->error('Attribute "'.$attributeWhitelistItem.'" does not exist in model '.$model); + } else { + $this->error('Option chunkSize needs to be an integer'); - return false; - } - } - } - array_push($modelAttributesToProcess, [ - 'model' => $model, - 'modelInstance' => $modelInstance, - 'attributes' => $attributes, - ]); + return 1; } } // Calculate - foreach ($modelAttributesToProcess as $modelAttributeToProcess) { - $this->info('Start calculating for following attributes of model "'.$modelAttributeToProcess['model'].'":'); - /** @var Model|ComputedAttributes $modelInstance */ - $modelInstance = $modelAttributeToProcess['modelInstance']; - $attributes = $modelAttributeToProcess['attributes']; - $this->info('['.implode(',', $attributes).']'); + foreach ($modelAttributesEntries as $modelAttributesEntry) { + $model = $modelAttributesEntry->getModel(); + $this->info('Start calculating for following attributes of model "' . $model . '":'); + + /** @var Builder|ComputedAttributes $modelInstance */ + $modelInstance = new $model(); + $attributes = $modelAttributesEntry->getAttributes(); + $this->info('[' . implode(',', $attributes) . ']'); if (sizeof($attributes) > 0) { $modelInstance->chunk($chunkSize, function ($modelResults) use ($attributes) { - /* @var Model|ComputedAttributes $modelInstance */ + /* @var Model|ComputedAttributes $modelResult */ foreach ($modelResults as $modelResult) { foreach ($attributes as $attribute) { $modelResult->setComputedAttributeValue($attribute); @@ -148,7 +102,6 @@ public function handle() }); } } - - return true; + return 0; } } diff --git a/src/Console/ValidateComputedAttributes.php b/src/Console/ValidateComputedAttributes.php new file mode 100644 index 0000000..bca7417 --- /dev/null +++ b/src/Console/ValidateComputedAttributes.php @@ -0,0 +1,111 @@ +modelAttributeParser = $modelAttributeParser; + parent::__construct(); + } + + /** + * Execute the console command. + * + * @return int + * @throws ReflectionException + */ + public function handle() + { + $modelsWithAttributes = $this->argument('modelsAttributes'); + + $this->info('Parsing arguments...'); + + // Validate and parse modelsAttributes argument + try { + $modelAttributesEntries = $this->modelAttributeParser->getModelAttributeEntries($modelsWithAttributes); + } catch (ParsingException $exception) { + $this->error($exception->getMessage()); + + return 1; + } + + // Validate chunkSize option + $chunkSizeRaw = $this->option('chunkSize'); + if ($chunkSizeRaw !== null) { + if (preg_match('/^\d+$/', $chunkSizeRaw)) { + $chunkSize = intval($chunkSizeRaw); + if ($chunkSize < 1) { + $this->error('Option chunkSize needs to be greater than zero'); + + return 1; + } + } else { + $this->error('Option chunkSize needs to be an integer'); + + return 1; + } + } + + // Validate + foreach ($modelAttributesEntries as $modelAttributesEntry) { + /** @var Model|ComputedAttributes $modelInstance */ + $modelInstance = ($modelAttributesEntry->getModel())(); + $attributes = $modelAttributesEntry->getAttributes(); + + $this->info('Start validating for following attributes of model "'.$modelAttributesEntry->getModel().'": '.implode(',', $attributes).''); + if (sizeof($attributes) > 0) { + $modelInstance->chunk($chunkSize, function ($modelResults) use ($attributes, $modelAttributesEntry) { + /* @var Model|ComputedAttributes $modelResult */ + foreach ($modelResults as $modelResult) { + foreach ($attributes as $attribute) { + if ($modelResult->getComputedAttributeValue($attribute) !== $modelResult->{$attribute}) { + $this->info($modelAttributesEntry->getModel().'['.$modelResult->getKeyName().'='.$modelResult->getKey().']['.$attribute.']'); + $this->info('Current value: '.$modelResult->{$attribute}); + $this->info('Calculated value: '.$modelResult->getComputedAttributeValue($attribute)); + } + } + } + }); + } + } + + return true; + } +} diff --git a/src/LaravelComputedAttributesServiceProvider.php b/src/LaravelComputedAttributesServiceProvider.php index 00cdb05..e3704c0 100644 --- a/src/LaravelComputedAttributesServiceProvider.php +++ b/src/LaravelComputedAttributesServiceProvider.php @@ -3,7 +3,7 @@ namespace Korridor\LaravelComputedAttributes; use Illuminate\Support\ServiceProvider; -use Korridor\LaravelComputedAttributes\Console\GenerateComputedAttributes; +use Korridor\LaravelComputedAttributes\Parser\ModelAttributeParser; /** * Class LaravelComputedAttributesServiceProvider. @@ -23,9 +23,21 @@ public function register() public function boot() { if ($this->app->runningInConsole()) { + $this->publishes([ + __DIR__ . '/../config/computed-attributes.php' => config_path('computed-attributes.php'), + ], 'computed-attributes-config'); $this->commands([ Console\GenerateComputedAttributes::class, ]); } + $this->app->bind(ModelAttributeParser::class, function () { + return new ModelAttributeParser(); + }); + if (!$this->app->configurationIsCached()) { + $this->mergeConfigFrom( + __DIR__ . '/../config/computed-attributes.php', + 'computed-attributes-config' + ); + } } } diff --git a/src/Parser/ModelAttributeParser.php b/src/Parser/ModelAttributeParser.php new file mode 100644 index 0000000..500e2dd --- /dev/null +++ b/src/Parser/ModelAttributeParser.php @@ -0,0 +1,106 @@ +getAbsolutePathOfModelFolder()); + $models = []; + foreach ($classmap as $class => $filepath) { + $reflection = new ReflectionClass($class); + $traits = $reflection->getTraitNames(); + foreach ($traits as $trait) { + if ('Korridor\\LaravelComputedAttributes\\ComputedAttributes' === $trait) { + array_push($models, $class); + } + } + } + return $models; + } + + /** + * @param string|null $modelsWithAttributes + * @return ModelAttributesEntry[] + * @throws ParsingException + * @throws ReflectionException + */ + public function getModelAttributeEntries(?string $modelsWithAttributes = null) + { + $modelAttributesToProcess = []; + $models = $this->getAllModelClasses(); + if (null === $modelsWithAttributes) { + foreach ($models as $model) { + /** @var Model|ComputedAttributes $modelInstance */ + $modelInstance = new $model(); + $attributes = $modelInstance->getComputedAttributeConfiguration(); + array_push($modelAttributesToProcess, new ModelAttributesEntry($model, $attributes)); + } + } else { + $modelsInAttribute = explode(';', $modelsWithAttributes); + foreach ($modelsInAttribute as $modelInAttribute) { + $modelInAttributeExploded = explode(':', $modelInAttribute); + if (1 !== sizeof($modelInAttributeExploded) && 2 !== sizeof($modelInAttributeExploded)) { + throw new ParsingException('Parsing error'); + } + $model = $this->getModelNamespaceBase().str_replace('/', '\\', $modelInAttributeExploded[0]); + if (in_array($model, $models)) { + /** @var Model|ComputedAttributes $modelInstance */ + $modelInstance = new $model(); + } else { + throw new ParsingException('Model "'.$model.'" not found'); + } + $attributes = $modelInstance->getComputedAttributeConfiguration(); + if (2 === sizeof($modelInAttributeExploded)) { + $attributeWhitelistItems = explode(',', $modelInAttributeExploded[1]); + foreach ($attributeWhitelistItems as $attributeWhitelistItem) { + if (!in_array($attributeWhitelistItem, $attributes)) { + throw new ParsingException('Attribute "'.$attributeWhitelistItem. + '" does not exist in model '.$model); + } + } + $attributes = $attributeWhitelistItems; + } + array_push($modelAttributesToProcess, new ModelAttributesEntry($model, $attributes)); + } + } + return $modelAttributesToProcess; + } +} diff --git a/src/Parser/ModelAttributesEntry.php b/src/Parser/ModelAttributesEntry.php new file mode 100644 index 0000000..46014fc --- /dev/null +++ b/src/Parser/ModelAttributesEntry.php @@ -0,0 +1,44 @@ +model = $model; + $this->attributes = $attributes; + } + + /** + * @return string + */ + public function getModel(): string + { + return $this->model; + } + + /** + * @return array + */ + public function getAttributes(): array + { + return $this->attributes; + } +} diff --git a/src/Parser/ParsingException.php b/src/Parser/ParsingException.php new file mode 100644 index 0000000..4ca72e6 --- /dev/null +++ b/src/Parser/ParsingException.php @@ -0,0 +1,10 @@ +title = 'titleTest'; + $post->content = 'Text'; + $post->save(); + $vote = new Vote(); + $vote->rating = 4; + $vote->post()->associate($post); + $vote->save(); + Config::set('computed-attributes.model_path', 'Models'); + Config::set('computed-attributes.model_namespace', 'Korridor\\LaravelComputedAttributes\\Tests\\TestEnvironment\\Models'); + $this->assertDatabaseHas('posts', [ + 'id' => $post->id, + 'complex_calculation' => null, + 'sum_of_votes' => 0, + ]); + + // Act + $this->artisan('computed-attributes:generate --chunkSize=100') + ->expectsOutput('Start calculating for following attributes of model ' . + '"Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post":') + ->expectsOutput('[complex_calculation,sum_of_votes]') + ->assertExitCode(0) + ->execute(); + + // Assert + $this->assertDatabaseHas('posts', [ + 'id' => $post->id, + 'complex_calculation' => 3, + 'sum_of_votes' => 4, + ]); + } + + public function testCommandCanOnlyCalculateOneAttributeOfOneModelIfSpecifiedInArgument() + { + // Arrange + $post = new Post(); + $post->title = 'titleTest'; + $post->content = 'Text'; + $post->save(); + $vote = new Vote(); + $vote->rating = 4; + $vote->post()->associate($post); + $vote->save(); + Config::set('computed-attributes.model_path', 'Models'); + Config::set('computed-attributes.model_namespace', 'Korridor\\LaravelComputedAttributes\\Tests\\TestEnvironment\\Models'); + $this->assertDatabaseHas('posts', [ + 'id' => $post->id, + 'complex_calculation' => null, + 'sum_of_votes' => 0, + ]); + + // Act + $this->artisan('computed-attributes:generate "Post:sum_of_votes" --chunkSize=100') + ->expectsOutput('Start calculating for following attributes of model ' . + '"Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post":') + ->expectsOutput('[sum_of_votes]') + ->assertExitCode(0) + ->execute(); + + // Assert + $this->assertDatabaseHas('posts', [ + 'id' => $post->id, + 'complex_calculation' => null, + 'sum_of_votes' => 4, + ]); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php new file mode 100644 index 0000000..b497418 --- /dev/null +++ b/tests/TestCase.php @@ -0,0 +1,33 @@ +loadMigrationsFrom(__DIR__ . '/TestEnvironment/Migrations'); + } + + protected function getEnvironmentSetUp($app) + { + $app->setBasePath(__DIR__ . '/TestEnvironment'); + } + + /** + * @param Application $app + * @return array + */ + protected function getPackageProviders($app) + { + return [ + LaravelComputedAttributesServiceProvider::class, + ]; + } +} diff --git a/tests/TestEnvironment/Migrations/0000_00_00_000000_add_posts_table.php b/tests/TestEnvironment/Migrations/0000_00_00_000000_add_posts_table.php new file mode 100644 index 0000000..b5af955 --- /dev/null +++ b/tests/TestEnvironment/Migrations/0000_00_00_000000_add_posts_table.php @@ -0,0 +1,36 @@ +increments('id'); + $table->string('title'); + $table->text('content'); + $table->integer('complex_calculation')->nullable(); + $table->integer('sum_of_votes'); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('posts'); + } +} diff --git a/tests/TestEnvironment/Migrations/0000_00_00_000001_add_votes_table.php b/tests/TestEnvironment/Migrations/0000_00_00_000001_add_votes_table.php new file mode 100644 index 0000000..f7e5ffd --- /dev/null +++ b/tests/TestEnvironment/Migrations/0000_00_00_000001_add_votes_table.php @@ -0,0 +1,39 @@ +increments('id'); + $table->integer('rating'); + $table->integer('post_id'); + $table->foreign('post_id') + ->on('posts') + ->references('id') + ->onUpdate('CASCADE') + ->onDelete('RESTRICT'); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + Schema::dropIfExists('votes'); + } +} diff --git a/tests/Models/Post.php b/tests/TestEnvironment/Models/Post.php similarity index 52% rename from tests/Models/Post.php rename to tests/TestEnvironment/Models/Post.php index fa4c960..c20cbd8 100644 --- a/tests/Models/Post.php +++ b/tests/TestEnvironment/Models/Post.php @@ -1,6 +1,9 @@ votes()->sum('rating'); + } + + /* + * Relations + */ + + /** + * @return HasMany|Vote + */ + public function votes(): HasMany + { + return $this->hasMany(Vote::class); + } + /** * Boot function from laravel. */ protected static function boot() { static::saving(function (Post $model) { - $model->setComputedAttributeValue('complex_calculation'); + $model->setComputedAttributeValue('sum_of_votes'); }); parent::boot(); } diff --git a/tests/TestEnvironment/Models/Vote.php b/tests/TestEnvironment/Models/Vote.php new file mode 100644 index 0000000..3492230 --- /dev/null +++ b/tests/TestEnvironment/Models/Vote.php @@ -0,0 +1,35 @@ +belongsTo(Post::class); + } + + /** + * Boot function from laravel. + */ + protected static function boot() + { + /* + static::saved(function (Vote $model) { + $model->post->setComputedAttributeValue('sum_of_votes'); + }); + */ + parent::boot(); + } +} From b018ea5f0bb297c064011bdfda75dfcc8eff5dad Mon Sep 17 00:00:00 2001 From: korridor <26689068+korridor@users.noreply.github.com> Date: Fri, 25 Sep 2020 15:50:58 +0200 Subject: [PATCH 2/9] Apply fixes from StyleCI (#2) Co-authored-by: korridor --- src/Console/GenerateComputedAttributes.php | 13 ++++++------- src/LaravelComputedAttributesServiceProvider.php | 6 +++--- src/Parser/ModelAttributeParser.php | 5 +++-- src/Parser/ModelAttributesEntry.php | 1 - src/Parser/ParsingException.php | 1 - .../GenerateComputedAttributesCommandTest.php | 8 ++------ tests/TestCase.php | 4 ++-- tests/TestEnvironment/Models/Vote.php | 1 - 8 files changed, 16 insertions(+), 23 deletions(-) diff --git a/src/Console/GenerateComputedAttributes.php b/src/Console/GenerateComputedAttributes.php index 1ea9db3..136063d 100644 --- a/src/Console/GenerateComputedAttributes.php +++ b/src/Console/GenerateComputedAttributes.php @@ -5,11 +5,9 @@ use Illuminate\Console\Command; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Illuminate\Support\Facades\Log; use Korridor\LaravelComputedAttributes\ComputedAttributes; use Korridor\LaravelComputedAttributes\Parser\ModelAttributeParser; use Korridor\LaravelComputedAttributes\Parser\ParsingException; -use Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post; use ReflectionException; /** @@ -22,8 +20,8 @@ class GenerateComputedAttributes extends Command * * @var string */ - protected $signature = 'computed-attributes:generate ' . - '{modelsAttributes? : List of models and optionally their attributes (example: "FullModel;PartModel:attribute_1,attribute_2" or "OtherNamespace\OtherModel")} ' . + protected $signature = 'computed-attributes:generate '. + '{modelsAttributes? : List of models and optionally their attributes (example: "FullModel;PartModel:attribute_1,attribute_2" or "OtherNamespace\OtherModel")} '. '{--chunkSize= : Size of the model chunk}'; /** @@ -58,7 +56,7 @@ public function handle() try { $modelAttributesEntries = $modelAttributeParser->getModelAttributeEntries($modelsWithAttributes); } catch (ParsingException $exception) { - var_dump($exception->getMessage()); + var_dump($exception->getMessage()); $this->error($exception->getMessage()); return 1; @@ -84,12 +82,12 @@ public function handle() // Calculate foreach ($modelAttributesEntries as $modelAttributesEntry) { $model = $modelAttributesEntry->getModel(); - $this->info('Start calculating for following attributes of model "' . $model . '":'); + $this->info('Start calculating for following attributes of model "'.$model.'":'); /** @var Builder|ComputedAttributes $modelInstance */ $modelInstance = new $model(); $attributes = $modelAttributesEntry->getAttributes(); - $this->info('[' . implode(',', $attributes) . ']'); + $this->info('['.implode(',', $attributes).']'); if (sizeof($attributes) > 0) { $modelInstance->chunk($chunkSize, function ($modelResults) use ($attributes) { /* @var Model|ComputedAttributes $modelResult */ @@ -102,6 +100,7 @@ public function handle() }); } } + return 0; } } diff --git a/src/LaravelComputedAttributesServiceProvider.php b/src/LaravelComputedAttributesServiceProvider.php index e3704c0..b7d7a93 100644 --- a/src/LaravelComputedAttributesServiceProvider.php +++ b/src/LaravelComputedAttributesServiceProvider.php @@ -24,7 +24,7 @@ public function boot() { if ($this->app->runningInConsole()) { $this->publishes([ - __DIR__ . '/../config/computed-attributes.php' => config_path('computed-attributes.php'), + __DIR__.'/../config/computed-attributes.php' => config_path('computed-attributes.php'), ], 'computed-attributes-config'); $this->commands([ Console\GenerateComputedAttributes::class, @@ -33,9 +33,9 @@ public function boot() $this->app->bind(ModelAttributeParser::class, function () { return new ModelAttributeParser(); }); - if (!$this->app->configurationIsCached()) { + if (! $this->app->configurationIsCached()) { $this->mergeConfigFrom( - __DIR__ . '/../config/computed-attributes.php', + __DIR__.'/../config/computed-attributes.php', 'computed-attributes-config' ); } diff --git a/src/Parser/ModelAttributeParser.php b/src/Parser/ModelAttributeParser.php index 500e2dd..d07f8c8 100644 --- a/src/Parser/ModelAttributeParser.php +++ b/src/Parser/ModelAttributeParser.php @@ -10,7 +10,6 @@ class ModelAttributeParser { - /** * ModelAttributeParser constructor. */ @@ -53,6 +52,7 @@ public function getAllModelClasses() } } } + return $models; } @@ -91,7 +91,7 @@ public function getModelAttributeEntries(?string $modelsWithAttributes = null) if (2 === sizeof($modelInAttributeExploded)) { $attributeWhitelistItems = explode(',', $modelInAttributeExploded[1]); foreach ($attributeWhitelistItems as $attributeWhitelistItem) { - if (!in_array($attributeWhitelistItem, $attributes)) { + if (! in_array($attributeWhitelistItem, $attributes)) { throw new ParsingException('Attribute "'.$attributeWhitelistItem. '" does not exist in model '.$model); } @@ -101,6 +101,7 @@ public function getModelAttributeEntries(?string $modelsWithAttributes = null) array_push($modelAttributesToProcess, new ModelAttributesEntry($model, $attributes)); } } + return $modelAttributesToProcess; } } diff --git a/src/Parser/ModelAttributesEntry.php b/src/Parser/ModelAttributesEntry.php index 46014fc..72eaccc 100644 --- a/src/Parser/ModelAttributesEntry.php +++ b/src/Parser/ModelAttributesEntry.php @@ -1,6 +1,5 @@ artisan('computed-attributes:generate --chunkSize=100') - ->expectsOutput('Start calculating for following attributes of model ' . + ->expectsOutput('Start calculating for following attributes of model '. '"Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post":') ->expectsOutput('[complex_calculation,sum_of_votes]') ->assertExitCode(0) @@ -72,7 +68,7 @@ public function testCommandCanOnlyCalculateOneAttributeOfOneModelIfSpecifiedInAr // Act $this->artisan('computed-attributes:generate "Post:sum_of_votes" --chunkSize=100') - ->expectsOutput('Start calculating for following attributes of model ' . + ->expectsOutput('Start calculating for following attributes of model '. '"Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post":') ->expectsOutput('[sum_of_votes]') ->assertExitCode(0) diff --git a/tests/TestCase.php b/tests/TestCase.php index b497418..49669ac 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -12,12 +12,12 @@ public function setUp(): void { parent::setUp(); - $this->loadMigrationsFrom(__DIR__ . '/TestEnvironment/Migrations'); + $this->loadMigrationsFrom(__DIR__.'/TestEnvironment/Migrations'); } protected function getEnvironmentSetUp($app) { - $app->setBasePath(__DIR__ . '/TestEnvironment'); + $app->setBasePath(__DIR__.'/TestEnvironment'); } /** diff --git a/tests/TestEnvironment/Models/Vote.php b/tests/TestEnvironment/Models/Vote.php index 3492230..d9001d2 100644 --- a/tests/TestEnvironment/Models/Vote.php +++ b/tests/TestEnvironment/Models/Vote.php @@ -7,7 +7,6 @@ class Vote extends Model { - /* * Relations */ From e9e8c5af1c175db53febe39487cb6fc7253bca12 Mon Sep 17 00:00:00 2001 From: korridor <26689068+korridor@users.noreply.github.com> Date: Fri, 25 Sep 2020 15:53:49 +0200 Subject: [PATCH 3/9] Removed var_dump --- src/Console/GenerateComputedAttributes.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Console/GenerateComputedAttributes.php b/src/Console/GenerateComputedAttributes.php index 136063d..6145798 100644 --- a/src/Console/GenerateComputedAttributes.php +++ b/src/Console/GenerateComputedAttributes.php @@ -56,7 +56,6 @@ public function handle() try { $modelAttributesEntries = $modelAttributeParser->getModelAttributeEntries($modelsWithAttributes); } catch (ParsingException $exception) { - var_dump($exception->getMessage()); $this->error($exception->getMessage()); return 1; From 73bb3ba1a1f0b06b144869294d306a133cf8cefe Mon Sep 17 00:00:00 2001 From: korridor <26689068+korridor@users.noreply.github.com> Date: Fri, 25 Sep 2020 15:57:13 +0200 Subject: [PATCH 4/9] Fixed bug --- src/ComputedAttributes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ComputedAttributes.php b/src/ComputedAttributes.php index de60c93..42f930d 100644 --- a/src/ComputedAttributes.php +++ b/src/ComputedAttributes.php @@ -2,7 +2,7 @@ namespace Korridor\LaravelComputedAttributes; -use Str; +use Illuminate\Support\Str; trait ComputedAttributes { From a4f80f46a6eb1b211cbeadfa752de2734c838b21 Mon Sep 17 00:00:00 2001 From: korridor <26689068+korridor@users.noreply.github.com> Date: Fri, 25 Sep 2020 16:16:06 +0200 Subject: [PATCH 5/9] Added legacy phpunit config --- .travis.yml | 54 ++++++++++++++++++++++++------------------------- phpunit.xml.old | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 27 deletions(-) create mode 100644 phpunit.xml.old diff --git a/.travis.yml b/.travis.yml index 8a64a41..0f1b162 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,62 +8,62 @@ matrix: include: # Laravel 5.6.* - php: 7.1 - env: LARAVEL='5.6.*' TESTBENCH='3.6.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='5.6.*' TESTBENCH='3.6.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' - php: 7.2 - env: LARAVEL='5.6.*' TESTBENCH='3.6.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='5.6.*' TESTBENCH='3.6.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' - php: 7.3 - env: LARAVEL='5.6.*' TESTBENCH='3.6.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='5.6.*' TESTBENCH='3.6.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' # Laravel 5.7.* - php: 7.1 - env: LARAVEL='5.7.*' TESTBENCH='3.7.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='5.7.*' TESTBENCH='3.7.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' - php: 7.2 - env: LARAVEL='5.7.*' TESTBENCH='3.7.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='5.7.*' TESTBENCH='3.7.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' - php: 7.3 - env: LARAVEL='5.7.*' TESTBENCH='3.7.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='5.7.*' TESTBENCH='3.7.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' # Laravel 5.8.* - php: 7.1 - env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' PHP_UNIT_CONFIG='phpunit.xml.old' - php: 7.1 - env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' - php: 7.2 - env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' PHP_UNIT_CONFIG='phpunit.xml.old' - php: 7.2 - env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' - php: 7.3 - env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-lowest' PHP_UNIT_CONFIG='phpunit.xml.old' - php: 7.3 - env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='5.8.*' TESTBENCH='3.8.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' # Laravel 6.* - php: 7.2 - env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-lowest' + env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-lowest' PHP_UNIT_CONFIG='phpunit.xml.old' - php: 7.2 - env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' - php: 7.3 - env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-lowest' + env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-lowest' PHP_UNIT_CONFIG='phpunit.xml' - php: 7.3 - env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='6.*' TESTBENCH='4.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml' # Laravel 7.* - php: 7.2 - env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-lowest' + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-lowest' PHP_UNIT_CONFIG='phpunit.xml.old' - php: 7.2 - env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' - php: 7.3 - env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-lowest' + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-lowest' PHP_UNIT_CONFIG='phpunit.xml' - php: 7.3 - env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml' - php: 7.4 - env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-lowest' + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-lowest' PHP_UNIT_CONFIG='phpunit.xml' - php: 7.4 - env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='7.*' TESTBENCH='5.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml' # Laravel 8.* - php: 7.3 - env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-lowest' + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-lowest' PHP_UNIT_CONFIG='phpunit.xml' - php: 7.3 - env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml' - php: 7.4 - env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-lowest' + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-lowest' PHP_UNIT_CONFIG='phpunit.xml' - php: 7.4 - env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-stable' + env: LARAVEL='8.*' TESTBENCH='6.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml' fast_finish: true before_install: @@ -77,7 +77,7 @@ before_script: - composer config discard-changes true script: - - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.xml + - vendor/bin/phpunit -c ${PHP_UNIT_CONFIG} --coverage-text --coverage-clover=coverage.xml after_success: - bash <(curl -s https://codecov.io/bash) diff --git a/phpunit.xml.old b/phpunit.xml.old new file mode 100644 index 0000000..e73bb3e --- /dev/null +++ b/phpunit.xml.old @@ -0,0 +1,36 @@ + + + + + tests/Feature + + + + + src/ + + + + + + + + + + + + + + + + + From bec770f86fb805b70dfb1585d690713db4c8ee5e Mon Sep 17 00:00:00 2001 From: korridor <26689068+korridor@users.noreply.github.com> Date: Fri, 25 Sep 2020 18:25:17 +0200 Subject: [PATCH 6/9] Added tests --- .travis.yml | 7 - composer.json | 6 +- readme.md | 8 +- src/ComputedAttributes.php | 44 +++++- src/Console/GenerateComputedAttributes.php | 55 ++++---- src/Console/ValidateComputedAttributes.php | 88 ++++++------ ...ravelComputedAttributesServiceProvider.php | 1 + .../GenerateComputedAttributesCommandTest.php | 48 ++++++- .../ValidateComputedAttributesCommandTest.php | 133 ++++++++++++++++++ tests/TestEnvironment/Models/Post.php | 58 +++++++- tests/TestEnvironment/Models/Vote.php | 13 +- 11 files changed, 364 insertions(+), 97 deletions(-) create mode 100644 tests/Feature/ValidateComputedAttributesCommandTest.php diff --git a/.travis.yml b/.travis.yml index 0f1b162..a836213 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,13 +6,6 @@ language: php matrix: include: - # Laravel 5.6.* - - php: 7.1 - env: LARAVEL='5.6.*' TESTBENCH='3.6.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' - - php: 7.2 - env: LARAVEL='5.6.*' TESTBENCH='3.6.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' - - php: 7.3 - env: LARAVEL='5.6.*' TESTBENCH='3.6.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' # Laravel 5.7.* - php: 7.1 env: LARAVEL='5.7.*' TESTBENCH='3.7.*' COMPOSER_FLAGS='--prefer-stable' PHP_UNIT_CONFIG='phpunit.xml.old' diff --git a/composer.json b/composer.json index 3332240..bf7c0e8 100644 --- a/composer.json +++ b/composer.json @@ -14,9 +14,9 @@ "require": { "php": "^7.1", "composer/composer": "^1.10", - "illuminate/console": "^5.6|^6|^7|^8", - "illuminate/database": "^5.6|^6|^7|^8", - "illuminate/support": "^5.6|^6|^7|^8" + "illuminate/console": "^5.7|^6|^7|^8", + "illuminate/database": "^5.7|^6|^7|^8", + "illuminate/support": "^5.7|^6|^7|^8" }, "require-dev": { "orchestra/testbench": "^3.6|^4.0|^5.0|^6.0", diff --git a/readme.md b/readme.md index 4a64d23..c4965e8 100644 --- a/readme.md +++ b/readme.md @@ -6,6 +6,11 @@ [![Codecov](https://img.shields.io/codecov/c/github/korridor/laravel-computed-attributes?style=flat-square)](https://codecov.io/gh/korridor/laravel-computed-attributes) [![StyleCI](https://styleci.io/repos/226346821/shield)](https://styleci.io/repos/226346821) +Laravel package that adds computed attributes to eloquent models. +A computed attribute is an accessor where the value is saved in the database. +The value can be regenerated or validated at any time. +This can increase performance (no calculation at every get/fetch) and it can simplify querying the database (f.e. complex filter system). +`` ## Installation You can install the package via composer with following command: @@ -23,11 +28,10 @@ This package is tested for the following Laravel versions: - 6.* - 5.8.* - 5.7.* (stable only) - - 5.6.* (stable only) ## Usage examples - +See folder `tests/TestEnvironment. ## Contributing diff --git a/src/ComputedAttributes.php b/src/ComputedAttributes.php index 42f930d..9989a65 100644 --- a/src/ComputedAttributes.php +++ b/src/ComputedAttributes.php @@ -2,35 +2,71 @@ namespace Korridor\LaravelComputedAttributes; +use Illuminate\Database\Eloquent\Builder; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Str; +/** + * @method static Builder|Model computedAttributesValidate(array $attributes) + * @method static Builder|Model computedAttributesGenerate(array $attributes) + */ trait ComputedAttributes { /** + * Compute the given attribute and return the result. + * * @param string $attributeName * @return mixed */ public function getComputedAttributeValue(string $attributeName) { $functionName = 'get'.Str::studly($attributeName).'Computed'; - $value = $this->{$functionName}(); - return $value; + return $this->{$functionName}(); } /** + * Compute the given attribute and assign the result in the model. + * * @param string $attributeName */ - public function setComputedAttributeValue(string $attributeName) + public function setComputedAttributeValue(string $attributeName): void { $computed = $this->getComputedAttributeValue($attributeName); $this->{$attributeName} = $computed; } /** + * This scope will be applied during the computed property generation with artisan computed-attributes:generate. + * + * @param Builder $builder + * @param array $attributes Attributes that will be generated. + * @return Builder + */ + public function scopeComputedAttributesGenerate(Builder $builder, array $attributes): Builder + { + return $builder; + } + + /** + * This scope will be applied during the computed property validation with artisan computed-attributes:validate. + * + * @param Builder $builder + * @param array $attributes Attributes that will be validated. + * @return Builder + */ + public function scopeComputedAttributesValidate(Builder $builder, array $attributes): Builder + { + return $builder; + } + + /** + * Return the configuration array for this model. + * If the configuration array does not exist the function will return an empty array. + * * @return array */ - public function getComputedAttributeConfiguration() + public function getComputedAttributeConfiguration(): array { if (isset($this->computed)) { return $this->computed; diff --git a/src/Console/GenerateComputedAttributes.php b/src/Console/GenerateComputedAttributes.php index 6145798..1496311 100644 --- a/src/Console/GenerateComputedAttributes.php +++ b/src/Console/GenerateComputedAttributes.php @@ -21,23 +21,17 @@ class GenerateComputedAttributes extends Command * @var string */ protected $signature = 'computed-attributes:generate '. - '{modelsAttributes? : List of models and optionally their attributes (example: "FullModel;PartModel:attribute_1,attribute_2" or "OtherNamespace\OtherModel")} '. - '{--chunkSize= : Size of the model chunk}'; + '{modelsAttributes? : List of models and optionally their attributes, '. + 'if not given all models that use the ComputedAttributes trait '. + '(example: "FullModel;PartModel:attribute_1,attribute_2" or "OtherNamespace\OtherModel")} '. + '{--chunkSize=500 : Size of the model chunk}'; /** * The console command description. * * @var string */ - protected $description = ''; - - /** - * Create a new command instance. - */ - public function __construct() - { - parent::__construct(); - } + protected $description = '(Re-)generates and saves the given computed attributes.'; /** * Execute the console command. @@ -63,40 +57,39 @@ public function handle() // Validate chunkSize option $chunkSizeRaw = $this->option('chunkSize'); - if ($chunkSizeRaw !== null) { - if (preg_match('/^\d+$/', $chunkSizeRaw)) { - $chunkSize = intval($chunkSizeRaw); - if ($chunkSize < 1) { - $this->error('Option chunkSize needs to be greater than zero'); - - return 1; - } - } else { - $this->error('Option chunkSize needs to be an integer'); + if (preg_match('/^\d+$/', $chunkSizeRaw)) { + $chunkSize = intval($chunkSizeRaw); + if ($chunkSize < 1) { + $this->error('Option chunkSize needs to be greater than zero'); return 1; } + } else { + $this->error('Option chunkSize needs to be an integer greater than zero'); + + return 1; } // Calculate foreach ($modelAttributesEntries as $modelAttributesEntry) { $model = $modelAttributesEntry->getModel(); - $this->info('Start calculating for following attributes of model "'.$model.'":'); - /** @var Builder|ComputedAttributes $modelInstance */ $modelInstance = new $model(); $attributes = $modelAttributesEntry->getAttributes(); + + $this->info('Start calculating for following attributes of model "'.$model.'":'); $this->info('['.implode(',', $attributes).']'); if (sizeof($attributes) > 0) { - $modelInstance->chunk($chunkSize, function ($modelResults) use ($attributes) { - /* @var Model|ComputedAttributes $modelResult */ - foreach ($modelResults as $modelResult) { - foreach ($attributes as $attribute) { - $modelResult->setComputedAttributeValue($attribute); + $modelInstance->computedAttributesGenerate($attributes) + ->chunk($chunkSize, function ($modelResults) use ($attributes) { + /* @var Model|ComputedAttributes $modelResult */ + foreach ($modelResults as $modelResult) { + foreach ($attributes as $attribute) { + $modelResult->setComputedAttributeValue($attribute); + } + $modelResult->save(); } - $modelResult->save(); - } - }); + }); } } diff --git a/src/Console/ValidateComputedAttributes.php b/src/Console/ValidateComputedAttributes.php index bca7417..b4598fb 100644 --- a/src/Console/ValidateComputedAttributes.php +++ b/src/Console/ValidateComputedAttributes.php @@ -3,6 +3,7 @@ namespace Korridor\LaravelComputedAttributes\Console; use Illuminate\Console\Command; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Korridor\LaravelComputedAttributes\ComputedAttributes; use Korridor\LaravelComputedAttributes\Parser\ModelAttributeParser; @@ -20,30 +21,17 @@ class ValidateComputedAttributes extends Command * @var string */ protected $signature = 'computed-attributes:validate '. - '{modelsAttributes? : List of models and optionally their attributes (example: "FullModel;PartModel:attribute_1,attribute_2" or "OtherNamespace/OtherModel")} '. - '{--chunkSize= : Size of the model chunk}'; + '{modelsAttributes? : List of models and optionally their attributes, '. + 'if not given all models that use the ComputedAttributes trait '. + '(example: "FullModel;PartModel:attribute_1,attribute_2" or "OtherNamespace/OtherModel")} '. + '{--chunkSize=500 : Size of the model chunk}'; /** * The console command description. * * @var string */ - protected $description = ''; - - /** - * @var ModelAttributeParser - */ - private $modelAttributeParser; - - /** - * Create a new command instance. - * @param ModelAttributeParser $modelAttributeParser - */ - public function __construct(ModelAttributeParser $modelAttributeParser) - { - $this->modelAttributeParser = $modelAttributeParser; - parent::__construct(); - } + protected $description = 'Validates the current values of the given computed attributes.'; /** * Execute the console command. @@ -58,8 +46,9 @@ public function handle() $this->info('Parsing arguments...'); // Validate and parse modelsAttributes argument + $modelAttributeParser = app(ModelAttributeParser::class); try { - $modelAttributesEntries = $this->modelAttributeParser->getModelAttributeEntries($modelsWithAttributes); + $modelAttributesEntries = $modelAttributeParser->getModelAttributeEntries($modelsWithAttributes); } catch (ParsingException $exception) { $this->error($exception->getMessage()); @@ -68,44 +57,59 @@ public function handle() // Validate chunkSize option $chunkSizeRaw = $this->option('chunkSize'); - if ($chunkSizeRaw !== null) { - if (preg_match('/^\d+$/', $chunkSizeRaw)) { - $chunkSize = intval($chunkSizeRaw); - if ($chunkSize < 1) { - $this->error('Option chunkSize needs to be greater than zero'); - - return 1; - } - } else { - $this->error('Option chunkSize needs to be an integer'); + if (preg_match('/^\d+$/', $chunkSizeRaw)) { + $chunkSize = intval($chunkSizeRaw); + if ($chunkSize < 1) { + $this->error('Option chunkSize needs to be greater than zero'); return 1; } + } else { + $this->error('Option chunkSize needs to be an integer greater than zero'); + + return 1; } // Validate foreach ($modelAttributesEntries as $modelAttributesEntry) { - /** @var Model|ComputedAttributes $modelInstance */ - $modelInstance = ($modelAttributesEntry->getModel())(); + $model = $modelAttributesEntry->getModel(); + /** @var Builder|ComputedAttributes $modelInstance */ + $modelInstance = new $model(); $attributes = $modelAttributesEntry->getAttributes(); - $this->info('Start validating for following attributes of model "'.$modelAttributesEntry->getModel().'": '.implode(',', $attributes).''); + $this->info('Start validating following attributes of model "'.$model.'":'); + $this->info('['.implode(',', $attributes).']'); if (sizeof($attributes) > 0) { - $modelInstance->chunk($chunkSize, function ($modelResults) use ($attributes, $modelAttributesEntry) { - /* @var Model|ComputedAttributes $modelResult */ - foreach ($modelResults as $modelResult) { - foreach ($attributes as $attribute) { - if ($modelResult->getComputedAttributeValue($attribute) !== $modelResult->{$attribute}) { - $this->info($modelAttributesEntry->getModel().'['.$modelResult->getKeyName().'='.$modelResult->getKey().']['.$attribute.']'); - $this->info('Current value: '.$modelResult->{$attribute}); - $this->info('Calculated value: '.$modelResult->getComputedAttributeValue($attribute)); + $modelInstance->computedAttributesValidate($attributes) + ->chunk($chunkSize, function ($modelResults) use ($attributes, $modelAttributesEntry) { + /* @var Model|ComputedAttributes $modelResult */ + foreach ($modelResults as $modelResult) { + foreach ($attributes as $attribute) { + if ($modelResult->getComputedAttributeValue($attribute) !== $modelResult->{$attribute}) { + $this->info($modelAttributesEntry->getModel(). + '['.$modelResult->getKeyName().'='.$modelResult->getKey().']['.$attribute.']'); + $this->info('Current value: '.$this->varToString($modelResult->{$attribute})); + $this->info('Calculated value: '. + $this->varToString($modelResult->getComputedAttributeValue($attribute))); + } } } - } - }); + }); } } return true; } + + /** + * @param $var + * @return false|string + */ + private function varToString($var) + { + if ($var === null) { + return 'null'; + } + return gettype($var).'('.$var.')'; + } } diff --git a/src/LaravelComputedAttributesServiceProvider.php b/src/LaravelComputedAttributesServiceProvider.php index b7d7a93..6e5e138 100644 --- a/src/LaravelComputedAttributesServiceProvider.php +++ b/src/LaravelComputedAttributesServiceProvider.php @@ -28,6 +28,7 @@ public function boot() ], 'computed-attributes-config'); $this->commands([ Console\GenerateComputedAttributes::class, + Console\ValidateComputedAttributes::class, ]); } $this->app->bind(ModelAttributeParser::class, function () { diff --git a/tests/Feature/GenerateComputedAttributesCommandTest.php b/tests/Feature/GenerateComputedAttributesCommandTest.php index 9cc8b83..80de23d 100644 --- a/tests/Feature/GenerateComputedAttributesCommandTest.php +++ b/tests/Feature/GenerateComputedAttributesCommandTest.php @@ -24,7 +24,10 @@ public function testCommandComputesAttributesForAllModelsWithTraitAndAllThereAtt $vote->post()->associate($post); $vote->save(); Config::set('computed-attributes.model_path', 'Models'); - Config::set('computed-attributes.model_namespace', 'Korridor\\LaravelComputedAttributes\\Tests\\TestEnvironment\\Models'); + Config::set( + 'computed-attributes.model_namespace', + 'Korridor\\LaravelComputedAttributes\\Tests\\TestEnvironment\\Models' + ); $this->assertDatabaseHas('posts', [ 'id' => $post->id, 'complex_calculation' => null, @@ -32,7 +35,9 @@ public function testCommandComputesAttributesForAllModelsWithTraitAndAllThereAtt ]); // Act - $this->artisan('computed-attributes:generate --chunkSize=100') + $this->artisan('computed-attributes:generate', [ + 'modelsAttributes' => null, + ]) ->expectsOutput('Start calculating for following attributes of model '. '"Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post":') ->expectsOutput('[complex_calculation,sum_of_votes]') @@ -59,7 +64,10 @@ public function testCommandCanOnlyCalculateOneAttributeOfOneModelIfSpecifiedInAr $vote->post()->associate($post); $vote->save(); Config::set('computed-attributes.model_path', 'Models'); - Config::set('computed-attributes.model_namespace', 'Korridor\\LaravelComputedAttributes\\Tests\\TestEnvironment\\Models'); + Config::set( + 'computed-attributes.model_namespace', + 'Korridor\\LaravelComputedAttributes\\Tests\\TestEnvironment\\Models' + ); $this->assertDatabaseHas('posts', [ 'id' => $post->id, 'complex_calculation' => null, @@ -67,7 +75,9 @@ public function testCommandCanOnlyCalculateOneAttributeOfOneModelIfSpecifiedInAr ]); // Act - $this->artisan('computed-attributes:generate "Post:sum_of_votes" --chunkSize=100') + $this->artisan('computed-attributes:generate', [ + 'modelsAttributes' => 'Post:sum_of_votes', + ]) ->expectsOutput('Start calculating for following attributes of model '. '"Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post":') ->expectsOutput('[sum_of_votes]') @@ -81,4 +91,34 @@ public function testCommandCanOnlyCalculateOneAttributeOfOneModelIfSpecifiedInAr 'sum_of_votes' => 4, ]); } + + public function testNonNumericChunkSizeIsReturnsErrorMessage() + { + $this->artisan('computed-attributes:generate', [ + '--chunkSize' => 'text' + ]) + ->expectsOutput('Option chunkSize needs to be an integer greater than zero') + ->assertExitCode(1) + ->execute(); + } + + public function testNegativeChunkSizeReturnsErrorMessage() + { + $this->artisan('computed-attributes:generate', [ + '--chunkSize' => '-10' + ]) + ->expectsOutput('Option chunkSize needs to be an integer greater than zero') + ->assertExitCode(1) + ->execute(); + } + + public function testZeroAsChunkSizeReturnsErrorMessage() + { + $this->artisan('computed-attributes:generate', [ + '--chunkSize' => '0' + ]) + ->expectsOutput('Option chunkSize needs to be greater than zero') + ->assertExitCode(1) + ->execute(); + } } diff --git a/tests/Feature/ValidateComputedAttributesCommandTest.php b/tests/Feature/ValidateComputedAttributesCommandTest.php new file mode 100644 index 0000000..7da75d2 --- /dev/null +++ b/tests/Feature/ValidateComputedAttributesCommandTest.php @@ -0,0 +1,133 @@ +title = 'titleTest'; + $post->content = 'Text'; + $post->save(); + $vote = new Vote(); + $vote->rating = 4; + $vote->post()->associate($post); + $vote->save(); + Config::set('computed-attributes.model_path', 'Models'); + Config::set( + 'computed-attributes.model_namespace', + 'Korridor\\LaravelComputedAttributes\\Tests\\TestEnvironment\\Models' + ); + $this->assertDatabaseHas('posts', [ + 'id' => $post->id, + 'complex_calculation' => null, + 'sum_of_votes' => 0, + ]); + + // Act + $this->artisan('computed-attributes:validate', [ + 'modelsAttributes' => null, + ]) + ->expectsOutput('Start validating following attributes of model '. + '"Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post":') + ->expectsOutput('[complex_calculation,sum_of_votes]') + ->expectsOutput('Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post[id=1][complex_calculation]') + ->expectsOutput('Current value: null') + ->expectsOutput('Calculated value: integer(3)') + ->expectsOutput('Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post[id=1][sum_of_votes]') + ->expectsOutput('Current value: integer(0)') + ->expectsOutput('Calculated value: integer(4)') + ->assertExitCode(0) + ->execute(); + + // Assert + $this->assertDatabaseHas('posts', [ + 'id' => $post->id, + 'complex_calculation' => null, + 'sum_of_votes' => 0, + ]); + } + + public function testCommandCanOnlyCalculateOneAttributeOfOneModelIfSpecifiedInArgument() + { + // Arrange + $post = new Post(); + $post->title = 'titleTest'; + $post->content = 'Text'; + $post->save(); + $vote = new Vote(); + $vote->rating = 4; + $vote->post()->associate($post); + $vote->save(); + Config::set('computed-attributes.model_path', 'Models'); + Config::set( + 'computed-attributes.model_namespace', + 'Korridor\\LaravelComputedAttributes\\Tests\\TestEnvironment\\Models' + ); + $this->assertDatabaseHas('posts', [ + 'id' => $post->id, + 'complex_calculation' => null, + 'sum_of_votes' => 0, + ]); + + // Act + $this->artisan('computed-attributes:validate', [ + 'modelsAttributes' => 'Post:sum_of_votes', + ]) + ->expectsOutput('Start validating following attributes of model '. + '"Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post":') + ->expectsOutput('[sum_of_votes]') + ->expectsOutput('Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post[id=1][sum_of_votes]') + ->expectsOutput('Current value: integer(0)') + ->expectsOutput('Calculated value: integer(4)') + ->assertExitCode(0) + ->execute(); + + // Assert + $this->assertDatabaseHas('posts', [ + 'id' => $post->id, + 'complex_calculation' => null, + 'sum_of_votes' => 0, + ]); + } + + public function testNonNumericChunkSizeIsReturnsErrorMessage() + { + $this->artisan('computed-attributes:validate', [ + '--chunkSize' => 'text' + ]) + ->expectsOutput('Option chunkSize needs to be an integer greater than zero') + ->assertExitCode(1) + ->execute(); + } + + public function testNegativeChunkSizeReturnsErrorMessage() + { + $this->artisan('computed-attributes:validate', [ + '--chunkSize' => '-10' + ]) + ->expectsOutput('Option chunkSize needs to be an integer greater than zero') + ->assertExitCode(1) + ->execute(); + } + + public function testZeroAsChunkSizeReturnsErrorMessage() + { + $this->artisan('computed-attributes:validate', [ + '--chunkSize' => '0' + ]) + ->expectsOutput('Option chunkSize needs to be greater than zero') + ->assertExitCode(1) + ->execute(); + } +} diff --git a/tests/TestEnvironment/Models/Post.php b/tests/TestEnvironment/Models/Post.php index c20cbd8..480a61d 100644 --- a/tests/TestEnvironment/Models/Post.php +++ b/tests/TestEnvironment/Models/Post.php @@ -2,6 +2,7 @@ namespace Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; use Korridor\LaravelComputedAttributes\ComputedAttributes; @@ -11,6 +12,9 @@ class Post extends Model use ComputedAttributes; /** + * The attributes that are computed. (f.e. for performance reasons) + * These attributes can be regenerated at any time. + * * @var array */ protected $computed = [ @@ -18,10 +22,24 @@ class Post extends Model 'sum_of_votes', ]; + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'complex_calculation' => 'int', + 'sum_of_votes' => 'int', + ]; + + /* + * Computed attributes. + */ + /** * @return int */ - public function getComplexCalculationComputed() + public function getComplexCalculationComputed(): int { return 1 + 2; } @@ -29,9 +47,43 @@ public function getComplexCalculationComputed() /** * @return int */ - public function getSumOfVotesComputed() + public function getSumOfVotesComputed(): int + { + return $this->votes->sum('rating'); + } + + /* + * Scopes. + */ + + /** + * This scope will be applied during the computed property generation with artisan computed-attributes:generate. + * + * @param Builder $builder + * @param array $attributes Attributes that will be generated. + * @return Builder + */ + public function scopeComputedAttributesGenerate(Builder $builder, array $attributes): Builder + { + if (in_array('sum_of_votes', $attributes)) { + return $builder->with('votes'); + } + return $builder; + } + + /** + * This scope will be applied during the computed property validation with artisan computed-attributes:validate. + * + * @param Builder $builder + * @param array $attributes Attributes that will be validated. + * @return Builder + */ + public function scopeComputedAttributesValidate(Builder $builder, array $attributes): Builder { - return $this->votes()->sum('rating'); + if (in_array('sum_of_votes', $attributes)) { + return $builder->with('votes'); + } + return $builder; } /* diff --git a/tests/TestEnvironment/Models/Vote.php b/tests/TestEnvironment/Models/Vote.php index d9001d2..8a24306 100644 --- a/tests/TestEnvironment/Models/Vote.php +++ b/tests/TestEnvironment/Models/Vote.php @@ -7,6 +7,16 @@ class Vote extends Model { + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'rating' => 'int', + ]; + /* * Relations */ @@ -25,10 +35,11 @@ public function post(): BelongsTo protected static function boot() { /* + Note: This listener is only commented out to test the commands on incorrect data. static::saved(function (Vote $model) { $model->post->setComputedAttributeValue('sum_of_votes'); }); - */ + */ parent::boot(); } } From d4ccd1580e40a39abea973d1f0810c918bd219e4 Mon Sep 17 00:00:00 2001 From: korridor <26689068+korridor@users.noreply.github.com> Date: Fri, 25 Sep 2020 18:28:02 +0200 Subject: [PATCH 7/9] Apply fixes from StyleCI (#3) Co-authored-by: korridor --- src/Console/ValidateComputedAttributes.php | 1 + .../GenerateComputedAttributesCommandTest.php | 20 +++++++++---------- .../ValidateComputedAttributesCommandTest.php | 20 +++++++++---------- tests/TestEnvironment/Models/Post.php | 2 ++ tests/TestEnvironment/Models/Vote.php | 1 - 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/Console/ValidateComputedAttributes.php b/src/Console/ValidateComputedAttributes.php index b4598fb..fcbcf1b 100644 --- a/src/Console/ValidateComputedAttributes.php +++ b/src/Console/ValidateComputedAttributes.php @@ -110,6 +110,7 @@ private function varToString($var) if ($var === null) { return 'null'; } + return gettype($var).'('.$var.')'; } } diff --git a/tests/Feature/GenerateComputedAttributesCommandTest.php b/tests/Feature/GenerateComputedAttributesCommandTest.php index 80de23d..6714a9c 100644 --- a/tests/Feature/GenerateComputedAttributesCommandTest.php +++ b/tests/Feature/GenerateComputedAttributesCommandTest.php @@ -36,8 +36,8 @@ public function testCommandComputesAttributesForAllModelsWithTraitAndAllThereAtt // Act $this->artisan('computed-attributes:generate', [ - 'modelsAttributes' => null, - ]) + 'modelsAttributes' => null, + ]) ->expectsOutput('Start calculating for following attributes of model '. '"Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post":') ->expectsOutput('[complex_calculation,sum_of_votes]') @@ -76,8 +76,8 @@ public function testCommandCanOnlyCalculateOneAttributeOfOneModelIfSpecifiedInAr // Act $this->artisan('computed-attributes:generate', [ - 'modelsAttributes' => 'Post:sum_of_votes', - ]) + 'modelsAttributes' => 'Post:sum_of_votes', + ]) ->expectsOutput('Start calculating for following attributes of model '. '"Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post":') ->expectsOutput('[sum_of_votes]') @@ -95,8 +95,8 @@ public function testCommandCanOnlyCalculateOneAttributeOfOneModelIfSpecifiedInAr public function testNonNumericChunkSizeIsReturnsErrorMessage() { $this->artisan('computed-attributes:generate', [ - '--chunkSize' => 'text' - ]) + '--chunkSize' => 'text', + ]) ->expectsOutput('Option chunkSize needs to be an integer greater than zero') ->assertExitCode(1) ->execute(); @@ -105,8 +105,8 @@ public function testNonNumericChunkSizeIsReturnsErrorMessage() public function testNegativeChunkSizeReturnsErrorMessage() { $this->artisan('computed-attributes:generate', [ - '--chunkSize' => '-10' - ]) + '--chunkSize' => '-10', + ]) ->expectsOutput('Option chunkSize needs to be an integer greater than zero') ->assertExitCode(1) ->execute(); @@ -115,8 +115,8 @@ public function testNegativeChunkSizeReturnsErrorMessage() public function testZeroAsChunkSizeReturnsErrorMessage() { $this->artisan('computed-attributes:generate', [ - '--chunkSize' => '0' - ]) + '--chunkSize' => '0', + ]) ->expectsOutput('Option chunkSize needs to be greater than zero') ->assertExitCode(1) ->execute(); diff --git a/tests/Feature/ValidateComputedAttributesCommandTest.php b/tests/Feature/ValidateComputedAttributesCommandTest.php index 7da75d2..632d7cb 100644 --- a/tests/Feature/ValidateComputedAttributesCommandTest.php +++ b/tests/Feature/ValidateComputedAttributesCommandTest.php @@ -36,8 +36,8 @@ public function testCommandComputesAttributesForAllModelsWithTraitAndAllThereAtt // Act $this->artisan('computed-attributes:validate', [ - 'modelsAttributes' => null, - ]) + 'modelsAttributes' => null, + ]) ->expectsOutput('Start validating following attributes of model '. '"Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post":') ->expectsOutput('[complex_calculation,sum_of_votes]') @@ -82,8 +82,8 @@ public function testCommandCanOnlyCalculateOneAttributeOfOneModelIfSpecifiedInAr // Act $this->artisan('computed-attributes:validate', [ - 'modelsAttributes' => 'Post:sum_of_votes', - ]) + 'modelsAttributes' => 'Post:sum_of_votes', + ]) ->expectsOutput('Start validating following attributes of model '. '"Korridor\LaravelComputedAttributes\Tests\TestEnvironment\Models\Post":') ->expectsOutput('[sum_of_votes]') @@ -104,8 +104,8 @@ public function testCommandCanOnlyCalculateOneAttributeOfOneModelIfSpecifiedInAr public function testNonNumericChunkSizeIsReturnsErrorMessage() { $this->artisan('computed-attributes:validate', [ - '--chunkSize' => 'text' - ]) + '--chunkSize' => 'text', + ]) ->expectsOutput('Option chunkSize needs to be an integer greater than zero') ->assertExitCode(1) ->execute(); @@ -114,8 +114,8 @@ public function testNonNumericChunkSizeIsReturnsErrorMessage() public function testNegativeChunkSizeReturnsErrorMessage() { $this->artisan('computed-attributes:validate', [ - '--chunkSize' => '-10' - ]) + '--chunkSize' => '-10', + ]) ->expectsOutput('Option chunkSize needs to be an integer greater than zero') ->assertExitCode(1) ->execute(); @@ -124,8 +124,8 @@ public function testNegativeChunkSizeReturnsErrorMessage() public function testZeroAsChunkSizeReturnsErrorMessage() { $this->artisan('computed-attributes:validate', [ - '--chunkSize' => '0' - ]) + '--chunkSize' => '0', + ]) ->expectsOutput('Option chunkSize needs to be greater than zero') ->assertExitCode(1) ->execute(); diff --git a/tests/TestEnvironment/Models/Post.php b/tests/TestEnvironment/Models/Post.php index 480a61d..ad67d3d 100644 --- a/tests/TestEnvironment/Models/Post.php +++ b/tests/TestEnvironment/Models/Post.php @@ -68,6 +68,7 @@ public function scopeComputedAttributesGenerate(Builder $builder, array $attribu if (in_array('sum_of_votes', $attributes)) { return $builder->with('votes'); } + return $builder; } @@ -83,6 +84,7 @@ public function scopeComputedAttributesValidate(Builder $builder, array $attribu if (in_array('sum_of_votes', $attributes)) { return $builder->with('votes'); } + return $builder; } diff --git a/tests/TestEnvironment/Models/Vote.php b/tests/TestEnvironment/Models/Vote.php index 8a24306..fd9988a 100644 --- a/tests/TestEnvironment/Models/Vote.php +++ b/tests/TestEnvironment/Models/Vote.php @@ -7,7 +7,6 @@ class Vote extends Model { - /** * The attributes that should be cast to native types. * From fdfa6c6d4494537b37e6028cfc94dae486a81cd2 Mon Sep 17 00:00:00 2001 From: korridor <26689068+korridor@users.noreply.github.com> Date: Fri, 25 Sep 2020 23:33:42 +0200 Subject: [PATCH 8/9] Fixed bug in ValidateComputedAttributes command --- src/Console/ValidateComputedAttributes.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Console/ValidateComputedAttributes.php b/src/Console/ValidateComputedAttributes.php index fcbcf1b..b1d922c 100644 --- a/src/Console/ValidateComputedAttributes.php +++ b/src/Console/ValidateComputedAttributes.php @@ -98,7 +98,7 @@ public function handle() } } - return true; + return 0; } /** From 29d314cf5562ae66e337ed3bb6505972592d8e6a Mon Sep 17 00:00:00 2001 From: korridor <26689068+korridor@users.noreply.github.com> Date: Sat, 26 Sep 2020 00:29:23 +0200 Subject: [PATCH 9/9] Fixed bugs --- readme.md | 2 +- src/LaravelComputedAttributesServiceProvider.php | 4 ++-- src/Parser/ModelAttributeParser.php | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/readme.md b/readme.md index c4965e8..2629383 100644 --- a/readme.md +++ b/readme.md @@ -31,7 +31,7 @@ This package is tested for the following Laravel versions: ## Usage examples -See folder `tests/TestEnvironment. +See folder `tests/TestEnvironment`. ## Contributing diff --git a/src/LaravelComputedAttributesServiceProvider.php b/src/LaravelComputedAttributesServiceProvider.php index 6e5e138..5d56e70 100644 --- a/src/LaravelComputedAttributesServiceProvider.php +++ b/src/LaravelComputedAttributesServiceProvider.php @@ -25,7 +25,7 @@ public function boot() if ($this->app->runningInConsole()) { $this->publishes([ __DIR__.'/../config/computed-attributes.php' => config_path('computed-attributes.php'), - ], 'computed-attributes-config'); + ], 'computed-attributes'); $this->commands([ Console\GenerateComputedAttributes::class, Console\ValidateComputedAttributes::class, @@ -37,7 +37,7 @@ public function boot() if (! $this->app->configurationIsCached()) { $this->mergeConfigFrom( __DIR__.'/../config/computed-attributes.php', - 'computed-attributes-config' + 'computed-attributes' ); } } diff --git a/src/Parser/ModelAttributeParser.php b/src/Parser/ModelAttributeParser.php index d07f8c8..a0f38e5 100644 --- a/src/Parser/ModelAttributeParser.php +++ b/src/Parser/ModelAttributeParser.php @@ -4,6 +4,7 @@ use Composer\Autoload\ClassMapGenerator; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Config; use Korridor\LaravelComputedAttributes\ComputedAttributes; use ReflectionClass; use ReflectionException; @@ -22,7 +23,7 @@ public function __construct() */ public function getAbsolutePathOfModelFolder(): string { - return base_path(config('computed-attributes.model_path')); + return base_path(Config::get('computed-attributes.model_path')); } /** @@ -30,7 +31,7 @@ public function getAbsolutePathOfModelFolder(): string */ public function getModelNamespaceBase(): string { - return config('computed-attributes.model_namespace').'\\'; + return Config::get('computed-attributes.model_namespace').'\\'; } /**