diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4803804..d862b24 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,46 +1,56 @@ name: CI on: - push: - pull_request: + push: + pull_request: jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - php: [8.1, 8.2, 8.3] - steps: - - uses: actions/checkout@v2 - - uses: shivammathur/setup-php@v2 - with: - php-version: ${{ matrix.php }} - tools: pecl - extensions: xdebug - - name: Show php version - run: php -v && composer -V - - name: Debug if needed - run: if [[ "$DEBUG" == "true" ]]; then env; fi - env: - DEBUG: ${{secrets.DEBUG}} - - name: Get Composer Cache Directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - name: Cache dependencies - uses: actions/cache@v1 - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ matrix.os }}-composer-${{ hashFiles('**/composer.json') }} - restore-keys: ${{ matrix.os }}-composer- - - name: Install dependencies - run: composer install --prefer-source - - name: Run unit tests - run: ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml - - name: Run Coverage - run: ./vendor/bin/php-coveralls -v - env: - COVERALLS_RUN_LOCALLY: ${{ secrets.COVERALLS_RUN_LOCALLY }} - COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} - continue-on-error: true - - name: show coverage json - run: cat build/logs/coveralls-upload.json + build: + runs-on: ubuntu-latest + strategy: + matrix: + php: [8.1, 8.2, 8.3] + stability: [prefer-stable] + + name: PHP ${{ matrix.php }} - ${{ matrix.stability }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup php + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: pecl + extensions: xdebug + + - name: Show php version + run: php -v && composer -V + + - name: Debug if needed + run: if [[ "$DEBUG" == "true" ]]; then env; fi + env: + DEBUG: ${{secrets.DEBUG}} + + - name: Install dependencies + run: composer install --prefer-source + + - name: PHP code standard + run: php vendor/bin/phpcs --standard=phpcs.xml src/ tests/ + + - name: PHP static analysis + run: php vendor/bin/phpstan analyse + + - name: Run unit tests + run: ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml + + - name: Run coverage + run: ./vendor/bin/php-coveralls -v + env: + COVERALLS_RUN_LOCALLY: ${{ secrets.COVERALLS_RUN_LOCALLY }} + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + continue-on-error: true + + - name: Show coverage json + run: cat build/logs/coveralls-upload.json diff --git a/composer.json b/composer.json index 95c7390..ad0cead 100644 --- a/composer.json +++ b/composer.json @@ -17,7 +17,11 @@ }, "require-dev": { "php-coveralls/php-coveralls": "^2.7.0", - "phpunit/phpunit": "^10.0" + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^10.0", + "slevomat/coding-standard": "^8.15", + "squizlabs/php_codesniffer": "^3.5", + "supportpal/coding-standard": "^0.4" }, "autoload": { "psr-4": { @@ -26,5 +30,10 @@ }, "autoload-dev": { "classmap": ["tests/stubs"] + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + } } } diff --git a/phpcs.xml b/phpcs.xml new file mode 100644 index 0000000..1fdb0cc --- /dev/null +++ b/phpcs.xml @@ -0,0 +1,5 @@ + + + Custom PHPCS config based on the SupportPal coding standard. + + diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..f062b86 --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,101 @@ +parameters: + ignoreErrors: + - + message: "#^Parameter \\#1 \\$callback of function call_user_func_array expects callable\\(\\)\\: mixed, array\\{static\\(Jenssegers\\\\Model\\\\Model\\), string\\} given\\.$#" + count: 1 + path: src/Model.php + + - + message: "#^Unsafe usage of new static\\(\\)\\.$#" + count: 4 + path: src/Model.php + + - + message: "#^Access to an undefined property Jenssegers\\\\Model\\\\Model\\:\\:\\$name\\.$#" + count: 1 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$active\\.$#" + count: 1 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$age\\.$#" + count: 1 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$bar\\.$#" + count: 1 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$birthday\\.$#" + count: 1 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$city\\.$#" + count: 3 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$collection_data\\.$#" + count: 1 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$count\\.$#" + count: 1 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$data\\.$#" + count: 1 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$default\\.$#" + count: 1 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$foo\\.$#" + count: 4 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$list_items\\.$#" + count: 2 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$name\\.$#" + count: 10 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$object_data\\.$#" + count: 1 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$password\\.$#" + count: 2 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$score\\.$#" + count: 1 + path: tests/ModelTest.php + + - + message: "#^Access to an undefined property ModelStub\\:\\:\\$secret\\.$#" + count: 2 + path: tests/ModelTest.php + + - + message: "#^Parameter \\#1 \\$callback of static method Jenssegers\\\\Model\\\\Model\\:\\:unguarded\\(\\) expects callable\\(\\)\\: mixed, array\\{PHPUnit\\\\Framework\\\\MockObject\\\\MockObject&stdClass, 'callback'\\} given\\.$#" + count: 1 + path: tests/ModelTest.php diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..b22a6db --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,10 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 8 + paths: + - src + - tests + parallel: + processTimeout: 300.0 diff --git a/src/Model.php b/src/Model.php index 2b8b21d..0473eab 100644 --- a/src/Model.php +++ b/src/Model.php @@ -8,40 +8,58 @@ use Illuminate\Support\Collection as BaseCollection; use JsonSerializable; +/** + * @implements ArrayAccess + * @implements Arrayable + */ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializable { /** * The model's attributes. + * + * @var mixed[] */ protected array $attributes = []; /** * The attributes that should be hidden for arrays. + * + * @var string[] */ protected array $hidden = []; /** * The attributes that should be visible in arrays. + * + * @var string[] */ protected array $visible = []; /** * The accessors to append to the model's array form. + * + * @var string[] */ protected array $appends = []; /** * The attributes that are mass assignable. + * + * @var string[] */ protected array $fillable = []; /** * The attributes that aren't mass assignable. + * + * @var string[] */ protected array $guarded = []; /** * The attributes that should be casted to native types. + * + * @var array */ protected array $casts = []; @@ -57,11 +75,15 @@ abstract class Model implements ArrayAccess, Arrayable, Jsonable, JsonSerializab /** * The cache of the mutated attributes for each class. + * + * @var mixed[] */ protected static array $mutatorCache = []; /** * Create a new Eloquent model instance. + * + * @param mixed[] $attributes */ public function __construct(array $attributes = []) { @@ -379,8 +401,9 @@ public function totallyGuarded(): bool * Convert the model instance to JSON. * * @param int $options + * @return string|false */ - public function toJson($options = 0): string + public function toJson($options = 0) { return json_encode($this->jsonSerialize(), $options); } @@ -821,8 +844,14 @@ public static function __callStatic(string $method, array $parameters) /** * Convert the model to its string representation. */ - public function __toString(): string + public function __toString() { - return $this->toJson(); + $string = $this->toJson(); + + if ($string === false) { + return ''; + } + + return $string; } } diff --git a/tests/ModelTest.php b/tests/ModelTest.php index a2cec0b..7caf108 100644 --- a/tests/ModelTest.php +++ b/tests/ModelTest.php @@ -4,7 +4,7 @@ class ModelTest extends TestCase { - public function testAttributeManipulation() + public function testAttributeManipulation(): void { $model = new ModelStub; $model->name = 'foo'; @@ -21,13 +21,13 @@ public function testAttributeManipulation() $this->assertFalse(isset($model['name'])); } - public function testConstructor() + public function testConstructor(): void { $model = new ModelStub(['name' => 'john']); $this->assertEquals('john', $model->name); } - public function testNewInstanceWithAttributes() + public function testNewInstanceWithAttributes(): void { $model = new ModelStub; $instance = $model->newInstance(['name' => 'john']); @@ -36,7 +36,7 @@ public function testNewInstanceWithAttributes() $this->assertEquals('john', $instance->name); } - public function testHidden() + public function testHidden(): void { $model = new ModelStub; $model->password = 'secret'; @@ -46,7 +46,7 @@ public function testHidden() $this->assertEquals(['password'], $model->getHidden()); } - public function testVisible() + public function testVisible(): void { $model = new ModelStub; $model->setVisible(['name']); @@ -57,7 +57,7 @@ public function testVisible() $this->assertEquals(['name' => 'John Doe'], $attributes); } - public function testToArray() + public function testToArray(): void { $model = new ModelStub; $model->name = 'foo'; @@ -79,7 +79,7 @@ public function testToArray() $this->assertTrue(isset($array['password'])); } - public function testToJson() + public function testToJson(): void { $model = new ModelStub; $model->name = 'john'; @@ -93,7 +93,7 @@ public function testToJson() $this->assertEquals(json_encode($object), (string) $model); } - public function testMutator() + public function testMutator(): void { $model = new ModelStub; $model->list_items = ['name' => 'john']; @@ -110,7 +110,7 @@ public function testMutator() $this->assertEquals(20, $model->age); } - public function testToArrayUsesMutators() + public function testToArrayUsesMutators(): void { $model = new ModelStub; $model->list_items = [1, 2, 3]; @@ -119,7 +119,7 @@ public function testToArrayUsesMutators() $this->assertEquals([1, 2, 3], $array['list_items']); } - public function testReplicate() + public function testReplicate(): void { $model = new ModelStub; $model->name = 'John Doe'; @@ -130,7 +130,7 @@ public function testReplicate() $this->assertEquals($model->name, $clone->name); } - public function testAppends() + public function testAppends(): void { $model = new ModelStub; $array = $model->toArray(); @@ -143,7 +143,7 @@ public function testAppends() $this->assertEquals('test', $array['test']); } - public function testArrayAccess() + public function testArrayAccess(): void { $model = new ModelStub; $model->name = 'John Doen'; @@ -153,7 +153,7 @@ public function testArrayAccess() $this->assertEquals($model->city, $model['city']); } - public function testSerialize() + public function testSerialize(): void { $model = new ModelStub; $model->name = 'john'; @@ -163,7 +163,7 @@ public function testSerialize() $this->assertEquals($model, unserialize($serialized)); } - public function testCasts() + public function testCasts(): void { $model = new ModelStub; $model->score = '0.34'; @@ -201,7 +201,7 @@ public function testCasts() $this->assertInstanceOf('\Illuminate\Support\Collection', $array['collection_data']); } - public function testGuarded() + public function testGuarded(): void { $model = new ModelStub(['secret' => 'foo']); $this->assertTrue($model->isGuarded('secret')); @@ -220,7 +220,7 @@ public function testGuarded() ModelStub::reguard(); } - public function testGuardedCallback() + public function testGuardedCallback(): void { ModelStub::unguard(); $mock = $this->getMockBuilder('stdClass') @@ -234,7 +234,7 @@ public function testGuardedCallback() ModelStub::reguard(); } - public function testTotallyGuarded() + public function testTotallyGuarded(): void { $this->expectException('Jenssegers\Model\MassAssignmentException'); @@ -244,7 +244,7 @@ public function testTotallyGuarded() $model->fill(['name' => 'John Doe']); } - public function testFillable() + public function testFillable(): void { $model = new ModelStub(['foo' => 'bar']); $this->assertFalse($model->isFillable('foo')); @@ -259,7 +259,7 @@ public function testFillable() $this->assertEquals('bar', $model->foo); } - public function testHydrate() + public function testHydrate(): void { $models = ModelStub::hydrate([['name' => 'John Doe']]); $this->assertEquals('John Doe', $models[0]->name); diff --git a/tests/stubs/ModelStub.php b/tests/stubs/ModelStub.php index 2c831c8..ca34f41 100644 --- a/tests/stubs/ModelStub.php +++ b/tests/stubs/ModelStub.php @@ -4,8 +4,10 @@ class ModelStub extends Model { - protected array $hidden = [ 'password']; + /** @var string[] */ + protected array $hidden = ['password']; + /** @var array */ protected array $casts = [ 'age' => 'integer', 'score' => 'float', @@ -18,10 +20,12 @@ class ModelStub extends Model 'foo' => 'bar', ]; + /** @var string[] */ protected array $guarded = [ 'secret', ]; + /** @var string[] */ protected array $fillable = [ 'name', 'city', @@ -35,34 +39,38 @@ class ModelStub extends Model 'collection_data', ]; - public function getListItemsAttribute($value) + public function getListItemsAttribute(mixed $value): mixed { return json_decode($value, true); } - public function setListItemsAttribute($value) + public function setListItemsAttribute(mixed $value): void { $this->attributes['list_items'] = json_encode($value); } - public function setBirthdayAttribute($value) + public function setBirthdayAttribute(mixed $value): void { $this->attributes['birthday'] = strtotime($value); } - public function getBirthdayAttribute($value) + public function getBirthdayAttribute(mixed $value): string { return date('Y-m-d', $value); } - public function getAgeAttribute($value) + public function getAgeAttribute(mixed $value): int { $date = DateTime::createFromFormat('U', $this->attributes['birthday']); + if ($date === false) { + return 0; + } + return $date->diff(new DateTime('now'))->y; } - public function getTestAttribute($value) + public function getTestAttribute(mixed $value): string { return 'test'; }