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';
}