diff --git a/.editorconfig b/.editorconfig index cd8eb86..a7c44dd 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,6 +1,3 @@ -; This file is for unifying the coding style for different editors and IDEs. -; More information at http://editorconfig.org - root = true [*] @@ -13,3 +10,6 @@ trim_trailing_whitespace = true [*.md] trim_trailing_whitespace = false + +[*.{yml,yaml}] +indent_size = 2 diff --git a/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 636ef53..fe5143b 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1,2 @@ +github: spatie custom: https://spatie.be/open-source/support-us diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..0d31fb8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,11 @@ +blank_issues_enabled: false +contact_links: + - name: Ask a question + url: https://github.com/vendor_slug/package_slug/discussions/new?category=q-a + about: Ask the community for help + - name: Request a feature + url: https://github.com/vendor_slug/package_slug/discussions/new?category=ideas + about: Share ideas for new features + - name: Report a bug + url: https://github.com/vendor_slug/package_slug/issues/new + about: Report a reproducable bug diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..ca91343 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,3 @@ +# Security Policy + +If you discover any security related issues, please email freek@spatie.be instead of using the issue tracker. diff --git a/.github/workflows/php-cs-fixer.yml b/.github/workflows/php-cs-fixer.yml index 84ab01a..7c5f3ee 100644 --- a/.github/workflows/php-cs-fixer.yml +++ b/.github/workflows/php-cs-fixer.yml @@ -13,7 +13,7 @@ jobs: - name: Fix style uses: docker://oskarstark/php-cs-fixer-ga with: - args: --config=.php_cs --allow-risky=yes + args: --config=.php_cs.dist --allow-risky=yes - name: Extract branch name shell: bash diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 0c4c494..283976f 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -8,25 +8,24 @@ jobs: strategy: fail-fast: true matrix: - php: [7.3, 7.4, 8.0] - laravel: [6.*, 7.*, 8.*] + php: [8.0] + laravel: [8.*] dependency-version: [prefer-lowest, prefer-stable] include: - laravel: 8.* - testbench: 6.* - - laravel: 7.* - testbench: 5.* - - laravel: 6.* - testbench: 4.* + testbench: '^6.15' name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} services: mysql: - image: mysql:5.7 + image: mysql:8.0 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes - MYSQL_DATABASE: laravel_tags + MYSQL_DATABASE: laravel_schemaless_attributes + MYSQL_ROOT_PASSWORD: root_password + MYSQL_USER: username + MYSQL_PASSWORD: password ports: - 3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 diff --git a/.gitignore b/.gitignore index 73c83f5..2e3dd49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,14 @@ +.idea +.php_cs +.php_cs.cache +.phpunit.result.cache build composer.lock +coverage docs +phpunit.xml +psalm.xml +testbench.yaml vendor -coverage -.env -.php_cs.cache +node_modules +.env \ No newline at end of file diff --git a/.php_cs b/.php_cs.dist similarity index 90% rename from .php_cs rename to .php_cs.dist index 9ab6e3d..c7d380c 100644 --- a/.php_cs +++ b/.php_cs.dist @@ -3,7 +3,7 @@ $finder = Symfony\Component\Finder\Finder::create() ->notPath('bootstrap/*') ->notPath('storage/*') - ->notPath('vendor') + ->notPath('resources/view/mail/*') ->in([ __DIR__ . '/src', __DIR__ . '/tests', @@ -31,12 +31,13 @@ return PhpCsFixer\Config::create() 'phpdoc_var_without_name' => true, 'class_attributes_separation' => [ 'elements' => [ - 'method', 'property', + 'method', ], ], 'method_argument_space' => [ 'on_multiline' => 'ensure_fully_multiline', 'keep_multiple_spaces_after_comma' => true, - ] + ], + 'single_trait_insert_per_statement' => true, ]) ->setFinder($finder); diff --git a/CHANGELOG.md b/CHANGELOG.md index f022951..5577a35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to `laravel-schemaless-attributes` will be documented in this file +## 2.0.0 - 2021-04-09 + +- require PHP 8+ +- use PHP 8 syntax where possible +- drop support for PHP 7 +- drop support for Laravel 6 +- implement `spatie/laravel-package-tools` +- implement custom class attribute casting + ## 1.8.3 - 2020-11-04 - allow PHP 8.0 diff --git a/README.md b/README.md index ffd2ca0..21babc0 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ $yourModel->extra_attributes->get('rey.side'); // returns 'light' // retrieve default value when attribute is not exists $yourModel->extra_attributes->get('non_existing', 'default'); // returns 'default' -// it has a scope to retrieve all models with the given schemaless attributes +// it has a modelScope to retrieve all models with the given schemaless attributes $yourModel->withSchemalessAttributes(['name' => 'value', 'name2' => 'value2'])->get(); // delete key & value @@ -56,6 +56,8 @@ This package requires a database with support for `json` columns like MySQL 5.7 ## Installation +> For Laravel versions 6 & 7 or PHP 7, use version 1.x of this package. + You can install the package via composer: ```bash @@ -76,29 +78,24 @@ Schema::table('your_models', function (Blueprint $table) { ### Preparing the model -In order to work with the schemaless attributes you'll need to add a cast, an accessor and a scopes on your model. Here's an example of what you need to add if you've chosen `extra_attributes` as your column name. +In order to work with the schemaless attributes you'll need to add a custom cast and a scope on your model. Here's an example of what you need to add if you've chosen `extra_attributes` as your column name. ```php use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; -use Spatie\SchemalessAttributes\SchemalessAttributes; +use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; class TestModel extends Model { // ... public $casts = [ - 'extra_attributes' => 'array', + 'extra_attributes' => SchemalessAttributes::class, ]; - public function getExtraAttributesAttribute(): SchemalessAttributes - { - return SchemalessAttributes::createForModel($this, 'extra_attributes'); - } - public function scopeWithExtraAttributes(): Builder { - return SchemalessAttributes::scopeWithSchemalessAttributes('extra_attributes'); + return $this->extra_attributes->modelCast(); } // ... @@ -129,12 +126,12 @@ class TestModel extends Model public function scopeWithExtraAttributes(): Builder { - return SchemalessAttributes::scopeWithSchemalessAttributes('extra_attributes'); + return $this->extra_attributes->modelScope(); } public function scopeWithOtherExtraAttributes(): Builder { - return SchemalessAttributes::scopeWithSchemalessAttributes('other_extra_attributes'); + return $this->other_extra_attributes->modelScope(); } // ... @@ -149,18 +146,18 @@ namespace App\Models\Concerns; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Builder; -use Spatie\SchemalessAttributes\SchemalessAttributes; +use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; trait HasSchemalessAttributes { - public function getExtraAttributesAttribute(): SchemalessAttributes + public function initializeHasSchemalessAttributesTrait() { - return SchemalessAttributes::createForModel($this, 'extra_attributes'); + $this->casts['extra_attributes'] = SchemalessAttributes::class; } - + public function scopeWithExtraAttributes(): Builder { - return SchemalessAttributes::scopeWithSchemalessAttributes('extra_attributes'); + return $this->extra_attributes->modelScope(); } } ``` @@ -221,14 +218,14 @@ $yourModel->save(); // Persists both normal and schemaless attributes ### Retrieving models with specific schemaless attributes -Here's how you can use the provided scope. +Here's how you can use the provided modelScope. ```php // Returns all models that have all the given schemaless attributes $yourModel->withExtraAttributes(['name' => 'value', 'name2' => 'value2'])->get(); ``` -If you only want to search on a single custom attribute, you can use the scope like this +If you only want to search on a single custom attribute, you can use the modelScope like this ```php // returns all models that have a schemaless attribute `name` set to `value` @@ -239,21 +236,21 @@ $yourModel->withExtraAttributes('name', 'value')->get(); First create a MySQL database named `laravel_schemaless_attributes`. After that you can run the tests with: -``` bash +```bash composer test ``` ## Changelog -Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. +Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. ## Contributing -Please see [CONTRIBUTING](CONTRIBUTING.md) for details. +Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. -## Security +## Security Vulnerabilities -If you discover any security related issues, please email freek@spatie.be instead of using the issue tracker. +Please review [our security policy](../../security/policy) on how to report security vulnerabilities. ## Credits diff --git a/composer.json b/composer.json index ec76d16..58e0225 100644 --- a/composer.json +++ b/composer.json @@ -16,14 +16,18 @@ } ], "require": { - "php": "^7.3|^8.0", - "illuminate/database": "^6.0|^7.0|^8.0", - "illuminate/support": "^6.0|^7.0|^8.0" + "php": "^8.0", + "illuminate/database": "^7.0|^8.0", + "illuminate/support": "^7.0|^8.0", + "spatie/laravel-package-tools": "^1.4.3", + "illuminate/contracts": "^7.0|^8.0" }, "require-dev": { "mockery/mockery": "^1.4", - "orchestra/testbench": "^4.0|^5.0|^6.0", - "phpunit/phpunit": "^9.4" + "orchestra/testbench": "^6.15", + "phpunit/phpunit": "^9.5.4", + "brianium/paratest": "^6.2", + "nunomaduro/collision": "^5.3" }, "autoload": { "psr-4": { @@ -36,7 +40,7 @@ } }, "scripts": { - "test": "vendor/bin/phpunit", + "test": "./vendor/bin/testbench package:test --parallel --no-coverage", "test-coverage": "vendor/bin/phpunit --coverage-html coverage", "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" }, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 276b8cc..d5dc1c8 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,20 +1,47 @@ - - - - src/ - - - - - tests - - - - - - - - - + + + + tests + + + + + ./src + + + + + + + + + + + + + + + + + + diff --git a/src/Casts/SchemalessAttributes.php b/src/Casts/SchemalessAttributes.php new file mode 100644 index 0000000..162c721 --- /dev/null +++ b/src/Casts/SchemalessAttributes.php @@ -0,0 +1,37 @@ +collection, $key, $default); } - /** - * @see Collection::set() - * - * @param $key - * @param $value - * - * @return mixed - */ - public function set($key, $value = null) + public function set($key, mixed $value = null): static { if (is_iterable($key)) { return $this->override($this->collection->merge($key)); @@ -106,14 +87,7 @@ public function set($key, $value = null) return $this->override(data_set($items, $key, $value)); } - /** - * @see Collection::forget() - * - * @param $keys - * - * @return SchemalessAttributes - */ - public function forget($keys) + public function forget($keys): static { $items = $this->collection->toArray(); @@ -124,7 +98,7 @@ public function forget($keys) return $this->override($items); } - public static function scopeWithSchemalessAttributes(string $attributeName): Builder + public function modelScope(): Builder { $arguments = debug_backtrace()[1]['args']; @@ -143,7 +117,7 @@ public static function scopeWithSchemalessAttributes(string $attributeName): Bui } foreach ($schemalessAttributes as $name => $value) { - $builder->where("{$attributeName}->{$name}", $value); + $builder->where("{$this->sourceAttributeName}->{$name}", $value); } return $builder; @@ -198,10 +172,10 @@ protected function getRawSchemalessAttributes(): array { $attributes = $this->model->getAttributes()[$this->sourceAttributeName] ?? '{}'; - return $attributes == '""' ? [] : $this->model->fromJson($attributes); + return $attributes === '""' ? [] : $this->model->fromJson($attributes); } - protected function override(iterable $collection) + protected function override(iterable $collection): static { $this->collection = new Collection($collection); $this->model->{$this->sourceAttributeName} = $this->collection->toArray(); diff --git a/src/SchemalessAttributesServiceProvider.php b/src/SchemalessAttributesServiceProvider.php index 8182478..457c9a2 100644 --- a/src/SchemalessAttributesServiceProvider.php +++ b/src/SchemalessAttributesServiceProvider.php @@ -3,11 +3,18 @@ namespace Spatie\SchemalessAttributes; use Illuminate\Database\Schema\Blueprint; -use Illuminate\Support\ServiceProvider; +use Spatie\LaravelPackageTools\Package; +use Spatie\LaravelPackageTools\PackageServiceProvider; -class SchemalessAttributesServiceProvider extends ServiceProvider +class SchemalessAttributesServiceProvider extends PackageServiceProvider { - public function register() + public function configurePackage(Package $package): void + { + $package + ->name('laravel-schemaless-attributes'); + } + + public function registeringPackage() { Blueprint::macro('schemalessAttributes', function (string $columnName = 'schemaless_attributes') { return $this->json($columnName)->nullable(); diff --git a/src/SchemalessAttributesTrait.php b/src/SchemalessAttributesTrait.php index c935554..49f3fd0 100644 --- a/src/SchemalessAttributesTrait.php +++ b/src/SchemalessAttributesTrait.php @@ -3,6 +3,7 @@ namespace Spatie\SchemalessAttributes; use Illuminate\Support\Collection; +use Spatie\SchemalessAttributes\Casts\SchemalessAttributes as SchemalessAttributesCast; /** * @property array $schemalessAttributes @@ -14,13 +15,15 @@ trait SchemalessAttributesTrait public function initializeSchemalessAttributesTrait() { foreach ($this->getSchemalessAttributes() as $attribute) { - $this->casts[$attribute] = 'array'; + $this->casts[$attribute] = SchemalessAttributesCast::class; } } public function getSchemalessAttributes(): array { - return isset($this->schemalessAttributes) ? $this->schemalessAttributes : []; + return isset($this->schemalessAttributes) + ? $this->schemalessAttributes + : []; } /** @@ -29,7 +32,7 @@ public function getSchemalessAttributes(): array */ public function __get($key) { - if (in_array($key, $this->getSchemalessAttributes())) { + if (in_array($key, $this->getSchemalessAttributes(), true)) { return SchemalessAttributes::createForModel($this, $key); } diff --git a/tests/SchemalessAttributesTraitTest.php b/tests/SchemalessAttributesTraitTest.php index be26b0d..d6735a0 100644 --- a/tests/SchemalessAttributesTraitTest.php +++ b/tests/SchemalessAttributesTraitTest.php @@ -2,7 +2,7 @@ namespace Spatie\SchemalessAttributes\Tests; -use Illuminate\Support\Str; +use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; class SchemalessAttributesTraitTest extends TestCase { @@ -19,21 +19,13 @@ public function setUp(): void /** @test */ public function schemaless_attributes_cast_as_array_initialize_schemaless_attributes_trait() { - if (Str::startsWith(app()->version(), '5.6')) { - $this->assertFalse($this->testModel->hasCast('schemaless_attributes', 'array')); - } else { - $this->assertTrue($this->testModel->hasCast('schemaless_attributes', 'array')); - } + $this->assertSame(SchemalessAttributes::class, $this->testModel->getCasts()['schemaless_attributes']); } /** @test */ public function other_schemaless_attributes_cast_as_array_initialize_schemaless_attributes_trait() { - if (Str::startsWith(app()->version(), '5.6')) { - $this->assertFalse($this->testModel->hasCast('other_schemaless_attributes', 'array')); - } else { - $this->assertTrue($this->testModel->hasCast('other_schemaless_attributes', 'array')); - } + $this->assertSame(SchemalessAttributes::class, $this->testModel->getCasts()['other_schemaless_attributes']); } /** @test */ diff --git a/tests/TestCase.php b/tests/TestCase.php index e9e0032..9f3ed1d 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -23,11 +23,25 @@ protected function getPackageProviders($app) ]; } + /* + * uses different table names for each test class to support + * running tests in parallel. + */ protected function setUpDatabase() { - Schema::dropIfExists('test_models'); + Schema::dropIfExists("test_models"); - Schema::create('test_models', function (Blueprint $table) { + Schema::create("test_models", function (Blueprint $table) { + $table->increments('id'); + $table->schemalessAttributes(); + }); + + $parts = explode('\\', static::class); + $class = array_pop($parts); + + Schema::dropIfExists("test_models_{$class}"); + + Schema::create("test_models_{$class}", function (Blueprint $table) { $table->increments('id'); $table->schemalessAttributes(); }); diff --git a/tests/TestModel.php b/tests/TestModel.php index 1dbfa7c..51d440d 100644 --- a/tests/TestModel.php +++ b/tests/TestModel.php @@ -4,7 +4,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; -use Spatie\SchemalessAttributes\SchemalessAttributes; +use Spatie\SchemalessAttributes\Casts\SchemalessAttributes; class TestModel extends Model { @@ -13,16 +13,11 @@ class TestModel extends Model public $guarded = []; public $casts = [ - 'schemaless_attributes' => 'array', + 'schemaless_attributes' => SchemalessAttributes::class, ]; - public function getSchemalessAttributesAttribute(): SchemalessAttributes - { - return SchemalessAttributes::createForModel($this, 'schemaless_attributes'); - } - public function scopeWithSchemalessAttributes(): Builder { - return SchemalessAttributes::scopeWithSchemalessAttributes('schemaless_attributes'); + return $this->schemaless_attributes->modelScope(); } } diff --git a/tests/docker-compose.yml b/tests/docker-compose.yml index a2ae883..1e852a6 100644 --- a/tests/docker-compose.yml +++ b/tests/docker-compose.yml @@ -2,7 +2,7 @@ version: "2" services: mysql: restart: always - image: mysql/mysql-server:5.7 + image: mysql/mysql-server:8.0 environment: MYSQL_ROOT_PASSWORD: "root_password" MYSQL_DATABASE: "laravel_schemaless_attributes" @@ -11,4 +11,4 @@ services: MYSQL_ROOT_HOST: "0.0.0.0" MYSQL_ALLOW_EMPTY_PASSWORD: "yes" ports: - - "3306:3306" \ No newline at end of file + - "3306:3306"