Compare commits

...
Author SHA1 Message Date
Jesper Noordsij ad7e37676f Bump branch alias to 3.6 (#1774) 2026-03-17 15:12:51 +01:00
Barry vd. Heuvel 9027494e97 Remove closure in attribute test (#1772) 2026-03-05 21:35:19 +01:00
isaackaara c3f795e909 fix: skip autoload exception when class existence is being checked (#1764)
The MetaCommand's custom autoloader throws a ReflectionException for any
class not found during meta generation. However, this breaks libraries
that use class_exists() to check for optional dependencies before loading
them (e.g. Doctrine ORM checking for StaticReflectionService removed in
doctrine/persistence v4).

This fix checks the call stack (up to 3 frames) for class_exists(),
interface_exists(), trait_exists(), or enum_exists() calls and returns
gracefully instead of throwing, allowing the existence check to return
false as expected.

Fixes #1750
2026-03-04 17:15:04 +01:00
2fba281b14 Fix PHPDoc placement before PHP 8 class attributes and add regression tests (#1769)
* Initial plan

* Add tests for PHP class attribute placement and fix whitespace in regex

Co-authored-by: barryvdh <[email protected]>

* Add advanced edge case tests: final+nested-brackets and multiple attributes

Co-authored-by: barryvdh <[email protected]>

* Add PHP 8.5 test for closures inside attribute arguments

Co-authored-by: barryvdh <[email protected]>

* composer fix-style

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: barryvdh <[email protected]>
Co-authored-by: laravel-ide-helper <[email protected]>
2026-03-04 12:00:42 +01:00
laravel-ide-helper 39bcb60b7c composer fix-style 2026-03-04 09:53:06 +00:00
isaackaara 16f685aa9f fix: don't extend root's parent class for facade stubs in helper file (#1766)
Since #1674 (commit 09623f2), the generated IDE helper file adds an
`extends` declaration for Macroable classes to expose inherited methods.
However, the check to skip facades only excluded classes in the
`\Illuminate\Support\Facades` namespace, so third-party facades (e.g.
`Spatie\Menu\Laravel\Facades\Menu`) would incorrectly extend the
facade root's parent class.

This caused the generated stub to conflict with the real facade class:

    namespace Spatie\Menu\Laravel\Facades {
        class Menu extends \Spatie\Menu\Menu {  // wrong!

The fix checks whether the extends class IS a Facade subclass (using
`is_subclass_of`) rather than comparing namespace strings. This
correctly excludes ALL facade classes (both Laravel and third-party)
while still allowing non-facade Macroable classes to extend their
parent.

Fixes #1724
2026-03-04 10:52:43 +01:00
10 changed files with 146 additions and 3 deletions
+1
View File
@@ -1,4 +1,5 @@
.phpunit.result.cache
/auth.json
/build
/.idea
/.php-cs-fixer.cache
+1 -1
View File
@@ -66,7 +66,7 @@
},
"extra": {
"branch-alias": {
"dev-master": "3.5-dev"
"dev-master": "3.6-dev"
},
"laravel": {
"providers": [
+1 -1
View File
@@ -187,7 +187,7 @@ class Alias
public function shouldExtendParentClass()
{
return $this->parentClass
&& $this->getExtendsNamespace() !== '\\Illuminate\\Support\\Facades';
&& !is_subclass_of($this->extends, Facade::class);
}
/**
+12
View File
@@ -195,6 +195,18 @@ class MetaCommand extends Command
return;
}
// Don't throw when class existence is being checked via class_exists(),
// interface_exists(), trait_exists(), or enum_exists(). These functions
// expect the autoloader to return gracefully when the class doesn't exist.
// Throwing here would break libraries that use class_exists() to check for
// optional dependencies (e.g. Doctrine ORM checking for removed classes).
$existsFunctions = ['class_exists', 'interface_exists', 'trait_exists', 'enum_exists'];
foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3) as $frame) {
if (isset($frame['function']) && in_array($frame['function'], $existsFunctions, true)) {
return;
}
}
throw new \ReflectionException("Class '$class' not found.");
};
+1 -1
View File
@@ -1206,7 +1206,7 @@ class ModelsCommand extends Command
// declaration, insert the docblock before the first attribute so that
// the resulting order is: docblock → attributes → class.
$before = substr($contents, 0, $pos);
if (preg_match('/(\s*(?:#\[.+?\]\s*)+)$/s', $before, $matches)) {
if (preg_match('/((?:#\[.+?\]\s*)+)$/s', $before, $matches)) {
$pos -= strlen($matches[1]);
$replace = "{$modelDocComment}\n";
}
@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\PhpAttributesBeforeClass\Models;
use Illuminate\Database\Eloquent\Model;
// Tests: final class + attribute with nested array argument (e.g. ObservedBy([...]))
#[ObservedByStub([StubObserver::class])]
final class FinalWithNested extends Model
{
protected $table = 'simples';
}
@@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\PhpAttributesBeforeClass\Models;
use Illuminate\Database\Eloquent\Model;
// Tests: multiple consecutive PHP 8 attributes before the class declaration
#[\AllowDynamicProperties]
#[SecondAttribute]
class MultipleAttributes extends Model
{
protected $table = 'simples';
}
@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\PhpAttributesBeforeClass\Models;
use Illuminate\Database\Eloquent\Model;
#[\AllowDynamicProperties]
class Simple extends Model
{
}
@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\PhpAttributesBeforeClass;
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();
}
}
@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\PhpAttributesBeforeClass\Models;
use Illuminate\Database\Eloquent\Model;
// Tests: final class + attribute with nested array argument (e.g. ObservedBy([...]))
/**
* @property int $id
* @method static \Illuminate\Database\Eloquent\Builder<static>|FinalWithNested newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|FinalWithNested newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|FinalWithNested query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|FinalWithNested whereId($value)
* @mixin \Eloquent
*/
#[ObservedByStub([StubObserver::class])]
final class FinalWithNested extends Model
{
protected $table = 'simples';
}
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\PhpAttributesBeforeClass\Models;
use Illuminate\Database\Eloquent\Model;
// Tests: multiple consecutive PHP 8 attributes before the class declaration
/**
* @property int $id
* @method static \Illuminate\Database\Eloquent\Builder<static>|MultipleAttributes newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|MultipleAttributes newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|MultipleAttributes query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|MultipleAttributes whereId($value)
* @mixin \Eloquent
*/
#[\AllowDynamicProperties]
#[SecondAttribute]
class MultipleAttributes extends Model
{
protected $table = 'simples';
}
<?php
declare(strict_types=1);
namespace Barryvdh\LaravelIdeHelper\Tests\Console\ModelsCommand\PhpAttributesBeforeClass\Models;
use Illuminate\Database\Eloquent\Model;
/**
* @property int $id
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simple newModelQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simple newQuery()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simple query()
* @method static \Illuminate\Database\Eloquent\Builder<static>|Simple whereId($value)
* @mixin \Eloquent
*/
#[\AllowDynamicProperties]
class Simple extends Model
{
}