Repository: barryvdh/laravel-ide-helper (php)

Issue:
Add support for the new cast types
### Summary

The casts types `immutable_date` and `immutable_datetime` were add sometime ago. But right now they get a `mixed` type when running the command for models.
Also, I think the encrypted casts don't work too.

Pull request description:
Improve casts support
## Summary
Provides support for all missing types for `Model::$casts` including `decimal:*`, `custom_datetime`, `encrypted:*`, `immutable_date`, and `immutable_custom_datetime`. Resolves #1257 

## Type of change

<!-- Please delete options that are not relevant. -->

- [x] Bug fix (non-breaking change which fixes an issue)
- [x] New feature (non-breaking change which adds functionality)

### Checklist
- [x] Existing tests have been adapted and/or new tests have been added
- [x] Add a CHANGELOG.md entry
- [ n/a ] Update the README.md
- [x] Code style has been fixed via `composer fix-style`


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index dc87c01..c95cf49 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file.
 
 [Next release](https://github.com/barryvdh/laravel-ide-helper/compare/v2.10.0...master)
 --------------
+### Added
+- Add support for cast types `decimal:*`, `encrypted:*`, `immutable_date`, `immutable_datetime`, `custom_datetime`, and `immutable_custom_datetime` [#1262 / miken32](https://github.com/barryvdh/laravel-ide-helper/pull/1262)
+
 ### Fixed
 - Fix recursively searching for `HasFactory` and `Macroable` traits [\#1216 / daniel-de-wit](https://github.com/barryvdh/laravel-ide-helper/pull/1216)
 
@@ -12,6 +15,7 @@ All notable changes to this project will be documented in this file.
 
 ### Added
 - Add support of variadic parameters in `ide-helper:models` [\#1234 / shaffe-fr](https://github.com/barryvdh/laravel-ide-helper/pull/1234)
+- Add support of custom casts without properties [\#1267 / sparclex](https://github.com/barryvdh/laravel-ide-helper/pull/1267)
 
 ### Changed
 - Move default models helper filename to config [\#1241 / wimski](https://github.com/barryvdh/laravel-ide-helper/pull/1241)
diff --git a/composer.json b/composer.json
index 00a0b4d..77ea124 100644
--- a/composer.json
+++ b/composer.json
@@ -23,7 +23,7 @@
         "php": "^7.3 || ^8.0",
         "ext-json": "*",
         "barryvdh/reflection-docblock": "^2.0.6",
-        "composer/composer": "^1.6 || ^2.0.13",
+        "composer/composer": "^1.10.23 || ^2.1.9",
         "doctrine/dbal": "^2.6 || ^3",
         "illuminate/console": "^8",
         "illuminate/filesystem": "^8",
diff --git a/src/Console/ModelsCommand.php b/src/Console/ModelsCommand.php
index 651f37e..c3c08c4 100644
--- a/src/Console/ModelsCommand.php
+++ b/src/Console/ModelsCommand.php
@@ -355,11 +355,24 @@ class ModelsCommand extends Command
     {
         $casts = $model->getCasts();
         foreach ($casts as $name => $type) {
+            if (Str::startsWith($type, 'decimal:')) {
+                $type = 'decimal';
+            } elseif (Str::startsWith($type, 'custom_datetime:')) {
+                $type = 'date';
+            } elseif (Str::startsWith($type, 'immutable_custom_datetime:')) {
+                $type = 'immutable_date';
+            } elseif (Str::startsWith($type, 'encrypted:')) {
+                $type = Str::after($type, ':');
+            }
             switch ($type) {
+                case 'encrypted':
+                    $realType = 'mixed';
+                    break;
                 case 'boolean':
                 case 'bool':
                     $realType = 'boolean';
                     break;
+                case 'decimal':
                 case 'string':
                     $realType = 'string';
                     break;
@@ -384,6 +397,10 @@ class ModelsCommand extends Command
                 case 'datetime':
                     $realType = $this->dateClass;
                     break;
+                case 'immutable_date':
+                case 'immutable_datetime':
+                    $realType = '\Carbon\CarbonImmutable';
+                    break;
                 case 'collection':
                     $realType = '\Illuminate\Support\Collection';
                     break;
@@ -392,6 +409,7 @@ class ModelsCommand extends Command
                     // the `$type` until the `:`
                     $type = strtok($type, ':');
                     $realType = class_exists($type) ? ('\\' . $type) : 'mixed';
+                    $this->setProperty($name, null, true, true);
                     break;
             }
 
@@ -918,6 +936,13 @@ class ModelsCommand extends Command
             if (!$phpdocMixin->getText()) {
                 $mixinDocComment = preg_replace("/\s\*\s*\n/", '', $mixinDocComment);
             }
+
+            foreach ($phpdoc->getTagsByName('mixin') as $tag) {
+                if (Str::startsWith($tag->getContent(), 'IdeHelper')) {
+                    $phpdoc->deleteTag($tag);
+                }
+            }
+            $docComment = $serializer->getDocComment($phpdoc);
         }
 
         if ($this->write) {
@@ -939,11 +964,15 @@ class ModelsCommand extends Command
         }
 
         $classname = $this->write_mixin ? $mixinClassName : $classname;
-        $output = "namespace {$namespace}{\n{$docComment}\n\t{$keyword}class {$classname} extends \Eloquent ";
+        $output = "namespace {$namespace}{\n{$docComment}\n\t{$keyword}class {$classname} ";
 
-        if ($interfaceNames) {
-            $interfaces = implode(', \\', $interfaceNames);
-            $output .= "implements \\{$interfaces} ";
+        if (!$this->write_mixin) {
+            $output .= "extends \Eloquent ";
+
+            if ($interfaceNames) {
+                $interfaces = implode(', \\', $interfaceNames);
+                $output .= "implements \\{$interfaces} ";
+            }
         }
 
         return $output . "{}\n}\n\n";
@@ -1323,7 +1352,7 @@ class ModelsCommand extends Command
             $docComment ?? '',
             $matches
         );
-        $type = $matches[1] ?? null;
+        $type = $matches[1] ?? '';
 
         if (strpos($type, '|') !== false) {
             $types = explode('|', $type);


Test patch:
diff --git a/.github/workflows/run-integration-tests.yml b/.github/workflows/run-integration-tests.yml
index cf342e4..99d77ca 100644
--- a/.github/workflows/run-integration-tests.yml
+++ b/.github/workflows/run-integration-tests.yml
@@ -18,7 +18,7 @@ jobs:
             COMPOSER_NO_INTERACTION: 1
         strategy:
             matrix:
-                php: [8.0, 7.4, 7.3]
+                php: [8.1, 8.0, 7.4, 7.3]
                 lumen: [8.*]
         name: P${{ matrix.php }} - Lumen${{ matrix.lumen }}
         steps:
@@ -75,7 +75,7 @@ jobs:
             COMPOSER_NO_INTERACTION: 1
         strategy:
             matrix:
-                php: [8.0, 7.4, 7.3]
+                php: [8.1, 8.0, 7.4, 7.3]
                 laravel: [8.*]
         name: P${{ matrix.php }} - Laravel${{ matrix.laravel }}
         steps:
diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index cc32341..0355288 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -20,7 +20,7 @@ jobs:
     strategy:
       matrix:
         os: [ubuntu-20.04, windows-2019]
-        php: [8.0, 7.4, 7.3]
+        php: [8.1, 8.0, 7.4, 7.3]
         laravel: [8.*]
         dependency-version: [prefer-lowest, prefer-stable]
         exclude:
diff --git a/tests/Console/EloquentCommandTest.php b/tests/Console/EloquentCommandTest.php
index 322cbcd..3776f7f 100644
--- a/tests/Console/EloquentCommandTest.php
+++ b/tests/Console/EloquentCommandTest.php
@@ -45,14 +45,14 @@ class EloquentCommandTest extends TestCase
         $this->assertMatchesTxtSnapshot($actualContent);
 
         $display = $tester->getDisplay();
-        $this->assertRegExp(
+        $this->assertMatchesRegularExpression(
             ';Unexpected no document on Illuminate\\\Database\\\Eloquent\\\Model;',
             $display
         );
         $modelClassFilePath = preg_quote(
             str_replace('/', DIRECTORY_SEPARATOR, '/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php')
         );
-        $this->assertRegExp(
+        $this->assertMatchesRegularExpression(
             ';Wrote expected docblock to .*' . $modelClassFilePath . ';',
             $display
         );
diff --git a/tests/Console/ModelsCommand/GeneratePhpdocWithMixin/__snapshots__/Test__test__1.php b/tests/Console/ModelsCommand/GeneratePhpdocWithMixin/__snapshots__/Test__test__1.php
index 872469a..88e7eb6 100644
--- a/tests/Console/ModelsCommand/GeneratePhpdocWithMixin/__snapshots__/Test__test__1.php
+++ b/tests/Console/ModelsCommand/GeneratePhpdocWithMixin/__snapshots__/Test__test__1.php
@@ -183,6 +183,6 @@ namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\GeneratePhpdocWi
  * @method static \Illuminate\Database\Eloquent\Builder|Post whereYearNullable($value)
  * @mixin \Eloquent
  */
-	class IdeHelperPost extends \Eloquent {}
+	class IdeHelperPost {}
 }
 
diff --git a/tests/Console/ModelsCommand/LaravelCustomCasts/Models/CustomCast.php b/tests/Console/ModelsCommand/LaravelCustomCasts/Models/CustomCast.php
index 6b7bf2b..c8876b8 100644
--- a/tests/Console/ModelsCommand/LaravelCustomCasts/Models/CustomCast.php
+++ b/tests/Console/ModelsCommand/LaravelCustomCasts/Models/CustomCast.php
@@ -34,5 +34,6 @@ class CustomCast extends Model
         'extended_casted_property_with_static_return_docblock' => ExtendedSelfCastingCasterWithStaticDocblockReturn::class,
         'extended_casted_property_with_this_return_docblock' => ExtendedSelfCastingCasterWithThisDocblockReturn::class,
         'casted_property_with_static_return_docblock_and_param' => SelfCastingCasterWithStaticDocblockReturn::class . ':param',
+        'cast_without_property' => CustomCasterWithReturnType::class,
     ];
 }
diff --git a/tests/Console/ModelsCommand/LaravelCustomCasts/__snapshots__/Test__test__1.php b/tests/Console/ModelsCommand/LaravelCustomCasts/__snapshots__/Test__test__1.php
index a108dbc..2bc8716 100644
--- a/tests/Console/ModelsCommand/LaravelCustomCasts/__snapshots__/Test__test__1.php
+++ b/tests/Console/ModelsCommand/LaravelCustomCasts/__snapshots__/Test__test__1.php
@@ -34,6 +34,7 @@ use Illuminate\Database\Eloquent\Model;
  * @property ExtendedSelfCastingCasterWithStaticDocblockReturn $extended_casted_property_with_static_return_docblock
  * @property ExtendedSelfCastingCasterWithThisDocblockReturn $extended_casted_property_with_this_return_docblock
  * @property SelfCastingCasterWithStaticDocblockReturn $casted_property_with_static_return_docblock_and_param
+ * @property \Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\LaravelCustomCasts\Casts\CastedProperty $cast_without_property
  * @method static \Illuminate\Database\Eloquent\Builder|CustomCast newModelQuery()
  * @method static \Illuminate\Database\Eloquent\Builder|CustomCast newQuery()
  * @method static \Illuminate\Database\Eloquent\Builder|CustomCast query()
@@ -68,5 +69,6 @@ class CustomCast extends Model
         'extended_casted_property_with_static_return_docblock' => ExtendedSelfCastingCasterWithStaticDocblockReturn::class,
         'extended_casted_property_with_this_return_docblock' => ExtendedSelfCastingCasterWithThisDocblockReturn::class,
         'casted_property_with_static_return_docblock_and_param' => SelfCastingCasterWithStaticDocblockReturn::class . ':param',
+        'cast_without_property' => CustomCasterWithReturnType::class,
     ];
 }
diff --git a/tests/Console/ModelsCommand/SimpleCasts/Models/SimpleCast.php b/tests/Console/ModelsCommand/SimpleCasts/Models/SimpleCast.php
new file mode 100644
index 0000000..5d8e3dc
--- /dev/null
+++ b/tests/Console/ModelsCommand/SimpleCasts/Models/SimpleCast.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SimpleCasts\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+class SimpleCast extends Model
+{
+    protected $casts = [
+        'cast_to_int' => 'int',
+        'cast_to_integer' => 'integer',
+        'cast_to_real' => 'real',
+        'cast_to_float' => 'float',
+        'cast_to_double' => 'double',
+        'cast_to_decimal' => 'decimal:4',
+        'cast_to_string' => 'string',
+        'cast_to_bool' => 'bool',
+        'cast_to_boolean' => 'boolean',
+        'cast_to_object' => 'object',
+        'cast_to_array' => 'array',
+        'cast_to_json' => 'json',
+        'cast_to_collection' => 'collection',
+        'cast_to_date' => 'date',
+        'cast_to_datetime' => 'datetime',
+        'cast_to_custom_datetime' => 'custom_datetime:Y-m-d H:i:s',
+        'cast_to_immutable_date' => 'immutable_date',
+        'cast_to_immutable_custom_datetime' => 'immutable_custom_datetime:Y-m-d H:i:s',
+        'cast_to_immutable_datetime' => 'immutable_datetime',
+        'cast_to_timestamp' => 'timestamp',
+        'cast_to_encrypted' => 'encrypted',
+        'cast_to_encrypted_array' => 'encrypted:array',
+        'cast_to_encrypted_collection' => 'encrypted:collection',
+        'cast_to_encrypted_json' => 'encrypted:json',
+        'cast_to_encrypted_object' => 'encrypted:object',
+    ];
+}
diff --git a/tests/Console/ModelsCommand/SimpleCasts/Test.php b/tests/Console/ModelsCommand/SimpleCasts/Test.php
new file mode 100644
index 0000000..05cb826
--- /dev/null
+++ b/tests/Console/ModelsCommand/SimpleCasts/Test.php
@@ -0,0 +1,24 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SimpleCasts;
+
+use Barryvdh\LaravelIdeHelper\Console\ModelsCommand;
+use Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\AbstractModelsCommand;
+
+class Test extends AbstractModelsCommand
+{
+    public function test(): void
+    {
+        $command = $this->app->make(ModelsCommand::class);
+
+        $tester = $this->runCommand($command, [
+            '--write' => true,
+        ]);
+
+        $this->assertSame(0, $tester->getStatusCode());
+        $this->assertStringContainsString('Written new phpDocBlock to', $tester->getDisplay());
+        $this->assertMatchesMockedSnapshot();
+    }
+}
diff --git a/tests/Console/ModelsCommand/SimpleCasts/__snapshots__/Test__test__1.php b/tests/Console/ModelsCommand/SimpleCasts/__snapshots__/Test__test__1.php
new file mode 100644
index 0000000..acb861f
--- /dev/null
+++ b/tests/Console/ModelsCommand/SimpleCasts/__snapshots__/Test__test__1.php
@@ -0,0 +1,96 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SimpleCasts\Models;
+
+use Illuminate\Database\Eloquent\Model;
+
+/**
+ * Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\SimpleCasts\Models\SimpleCast
+ *
+ * @property integer $cast_to_int
+ * @property integer $cast_to_integer
+ * @property float $cast_to_real
+ * @property float $cast_to_float
+ * @property float $cast_to_double
+ * @property string $cast_to_decimal
+ * @property string $cast_to_string
+ * @property boolean $cast_to_bool
+ * @property boolean $cast_to_boolean
+ * @property object $cast_to_object
+ * @property array $cast_to_array
+ * @property array $cast_to_json
+ * @property \Illuminate\Support\Collection $cast_to_collection
+ * @property \Illuminate\Support\Carbon $cast_to_date
+ * @property \Illuminate\Support\Carbon $cast_to_datetime
+ * @property \Illuminate\Support\Carbon $cast_to_custom_datetime
+ * @property \Carbon\CarbonImmutable $cast_to_immutable_date
+ * @property \Carbon\CarbonImmutable $cast_to_immutable_custom_datetime
+ * @property \Carbon\CarbonImmutable $cast_to_immutable_datetime
+ * @property integer $cast_to_timestamp
+ * @property mixed $cast_to_encrypted
+ * @property array $cast_to_encrypted_array
+ * @property \Illuminate\Support\Collection $cast_to_encrypted_collection
+ * @property array $cast_to_encrypted_json
+ * @property object $cast_to_encrypted_object
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast newModelQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast newQuery()
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast query()
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToArray($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToBool($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToBoolean($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToCollection($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToCustomDatetime($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToDate($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToDatetime($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToDecimal($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToDouble($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToEncrypted($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToEncryptedArray($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToEncryptedCollection($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToEncryptedJson($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToEncryptedObject($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToFloat($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToImmutableCustomDatetime($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToImmutableDate($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToImmutableDatetime($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToInt($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToInteger($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToJson($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToObject($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToReal($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToString($value)
+ * @method static \Illuminate\Database\Eloquent\Builder|SimpleCast whereCastToTimestamp($value)
+ * @mixin \Eloquent
+ */
+class SimpleCast extends Model
+{
+    protected $casts = [
+        'cast_to_int' => 'int',
+        'cast_to_integer' => 'integer',
+        'cast_to_real' => 'real',
+        'cast_to_float' => 'float',
+        'cast_to_double' => 'double',
+        'cast_to_decimal' => 'decimal:4',
+        'cast_to_string' => 'string',
+        'cast_to_bool' => 'bool',
+        'cast_to_boolean' => 'boolean',
+        'cast_to_object' => 'object',
+        'cast_to_array' => 'array',
+        'cast_to_json' => 'json',
+        'cast_to_collection' => 'collection',
+        'cast_to_date' => 'date',
+        'cast_to_datetime' => 'datetime',
+        'cast_to_custom_datetime' => 'custom_datetime:Y-m-d H:i:s',
+        'cast_to_immutable_date' => 'immutable_date',
+        'cast_to_immutable_custom_datetime' => 'immutable_custom_datetime:Y-m-d H:i:s',
+        'cast_to_immutable_datetime' => 'immutable_datetime',
+        'cast_to_timestamp' => 'timestamp',
+        'cast_to_encrypted' => 'encrypted',
+        'cast_to_encrypted_array' => 'encrypted:array',
+        'cast_to_encrypted_collection' => 'encrypted:collection',
+        'cast_to_encrypted_json' => 'encrypted:json',
+        'cast_to_encrypted_object' => 'encrypted:object',
+    ];
+}
diff --git a/tests/Console/ModelsCommand/migrations/____simple_casts_table.php b/tests/Console/ModelsCommand/migrations/____simple_casts_table.php
new file mode 100644
index 0000000..76b8f3a
--- /dev/null
+++ b/tests/Console/ModelsCommand/migrations/____simple_casts_table.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class SimpleCastsTable extends Migration
+{
+    public function up(): void
+    {
+        Schema::create('simple_casts', static function (Blueprint $table) {
+            $table->string('cast_to_int');
+            $table->string('cast_to_integer');
+            $table->string('cast_to_real');
+            $table->string('cast_to_float');
+            $table->string('cast_to_double');
+            $table->string('cast_to_decimal');
+            $table->string('cast_to_string');
+            $table->string('cast_to_bool');
+            $table->string('cast_to_boolean');
+            $table->string('cast_to_object');
+            $table->string('cast_to_array');
+            $table->string('cast_to_json');
+            $table->string('cast_to_collection');
+            $table->string('cast_to_date');
+            $table->string('cast_to_datetime');
+            $table->string('cast_to_custom_datetime');
+            $table->string('cast_to_immutable_date');
+            $table->string('cast_to_immutable_custom_datetime');
+            $table->string('cast_to_immutable_datetime');
+            $table->string('cast_to_timestamp');
+            $table->string('cast_to_encrypted');
+            $table->string('cast_to_encrypted_array');
+            $table->string('cast_to_encrypted_collection');
+            $table->string('cast_to_encrypted_json');
+            $table->string('cast_to_encrypted_object');
+        });
+    }
+}