Skip to content

Commit

Permalink
Merge pull request #327 from dedoc/l11-support
Browse files Browse the repository at this point in the history
Added Laravel 11 Support
  • Loading branch information
romalytvynenko authored Mar 9, 2024
2 parents f0331d5 + 40e8959 commit 6bd44d3
Show file tree
Hide file tree
Showing 42 changed files with 89 additions and 131 deletions.
13 changes: 7 additions & 6 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,17 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest]
php: [8.1]
laravel: [10.*, 9.*, 8.81.*]
php: [8.1, 8.2]
laravel: [11.*, 10.*]
stability: [prefer-lowest, prefer-stable]
include:
- laravel: 10.*
testbench: 8.*
- laravel: 9.*
testbench: 7.*
- laravel: 8.81.*
testbench: 6.24.*
- laravel: 11.*
testbench: 9.*
exclude:
- laravel: 11.*
php: 8.1

name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }}

Expand Down
17 changes: 8 additions & 9 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,19 @@
],
"require": {
"php": "^8.1",
"illuminate/contracts": "^8.0.0|^9.0.0|^10.0.0",
"nikic/php-parser": "^4.0",
"illuminate/contracts": "^10.0|^11.0",
"nikic/php-parser": "^5.0",
"phpstan/phpdoc-parser": "^1.0",
"spatie/laravel-package-tools": "^1.9.2"
},
"require-dev": {
"doctrine/dbal": "^3.4",
"laravel/pint": "^v1.1.0",
"nunomaduro/collision": "^5.0|^v6.0",
"orchestra/testbench": "^6.0|^7.0|^8.0",
"pestphp/pest": "^1.21",
"pestphp/pest-plugin-laravel": "^1.2",
"phpunit/phpunit": "^9.5",
"spatie/pest-plugin-snapshots": "^1.1"
"nunomaduro/collision": "^7.0|^8.0",
"orchestra/testbench": "^8.0|^9.0",
"pestphp/pest": "^2.34",
"pestphp/pest-plugin-laravel": "^2.3",
"phpunit/phpunit": "^10.5",
"spatie/pest-plugin-snapshots": "^2.1"
},
"autoload": {
"psr-4": {
Expand Down
4 changes: 2 additions & 2 deletions src/Infer/Handler/ThrowHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,10 @@ class ThrowHandler
{
public function shouldHandle($node)
{
return $node instanceof Node\Stmt\Throw_;
return $node instanceof Node\Expr\Throw_;
}

public function leave(Node\Stmt\Throw_ $node, Scope $scope)
public function leave(Node\Expr\Throw_ $node, Scope $scope)
{
if (! $scope->isInFunction()) {
return;
Expand Down
2 changes: 2 additions & 0 deletions src/Infer/Reflector/ClassReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ class ClassReflector

private ?NameContext $nameContext = null;

private array $methods = [];

private function __construct(
private FileParser $parser,
private string $className,
Expand Down
2 changes: 1 addition & 1 deletion src/Infer/Reflector/MethodReflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ public function __construct($nameContext)
$this->nameContext = $nameContext;
}

public function beforeTraverse(array $nodes)
public function beforeTraverse(array $nodes): ?array
{
return null;
}
Expand Down
2 changes: 1 addition & 1 deletion src/ScrambleServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public function configurePackage(Package $package): void

$this->app->singleton(FileParser::class, function () {
return new FileParser(
(new ParserFactory)->create(ParserFactory::PREFER_PHP7)
(new ParserFactory)->createForHostVersion()
);
});

Expand Down
14 changes: 5 additions & 9 deletions src/Support/InferExtensions/JsonResourceTypeInfer.php
Original file line number Diff line number Diff line change
Expand Up @@ -147,17 +147,13 @@ private static function modelType(ClassDefinition $jsonClass, Scope $scope): ?Ty
$modelType = new UnknownType("Cannot resolve [$modelClass] model type.");
$modelClassDefinition = null;
if ($modelClass && is_a($modelClass, Model::class, true)) {
try {
$modelClassDefinition = (new ModelInfo($modelClass))->type();
// @todo Use ModelExtension implementation of model info to type conversion.
// @todo The problem is that model extension type is dynamic and I'm not sure how to use it here.
$modelClassDefinition = (new ModelInfo($modelClass))->type();

$scope->index->registerClassDefinition($modelClassDefinition);
$scope->index->registerClassDefinition($modelClassDefinition);

$modelType = new ObjectType($modelClassDefinition->name);
} catch (\LogicException $e) {
// Here doctrine/dbal is not installed.
$modelType = null;
$modelClassDefinition = null;
}
$modelType = new ObjectType($modelClassDefinition->name);
}

static::$jsonResourcesModelTypesCache[$jsonClass->name] = [$modelType, $modelClassDefinition];
Expand Down
14 changes: 9 additions & 5 deletions src/Support/InferExtensions/ModelExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,18 +68,22 @@ public function getPropertyType(PropertyFetchEvent $event): ?Type

private function getBaseAttributeType(Model $model, string $key, array $value)
{
$type = explode(' ', $value['type']);
$typeName = explode('(', $type[0])[0];
$type = explode(' ', $value['type'] ?? '');
$typeName = explode('(', $type[0] ?? '')[0];

if (in_array($key, $model->getDates())) {
if (
($model->getCasts()[$key] ?? null) === 'datetime'
|| in_array($key, $model->getDates())
) {
return new ObjectType(Carbon::class);
}

// @todo Fix to native types
$attributeType = match ($typeName) {
'int', 'integer', 'bigint' => new IntegerType(),
'float', 'double', 'decimal' => new FloatType(),
'string', 'text', 'datetime' => new StringType(),
'bool', 'boolean' => new BooleanType(),
'varchar', 'string', 'text', 'datetime' => new StringType(), // string, text - needed?
'tinyint', 'bool', 'boolean' => new BooleanType(), // bool, boolean - needed?
'json', 'array' => new ArrayType(),
default => new UnknownType("unimplemented DB column type [$type[0]]"),
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,13 @@ private function getPossibleParamType(Node\Stmt\ClassMethod $methodNode, Node\Ex
{
$paramsMap = collect($methodNode->getParams())
->mapWithKeys(function (Node\Param $param) {
if (! $param->type) {
return [];
}

try {
return [
$param->var->name => implode('\\', $param->type->parts ?? []),
$param->var->name => $param->type->name,
];
} catch (\Throwable $exception) {
throw $exception;
Expand Down
136 changes: 48 additions & 88 deletions src/Support/ResponseExtractor/ModelInfo.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Dedoc\Scramble\Support\ResponseExtractor;

use BackedEnum;
use Dedoc\Scramble\Infer\Definition\ClassDefinition;
use Dedoc\Scramble\Infer\Definition\ClassPropertyDefinition;
use Dedoc\Scramble\Support\Type\ArrayType;
Expand All @@ -14,17 +15,18 @@
use Dedoc\Scramble\Support\Type\StringType;
use Dedoc\Scramble\Support\Type\Union;
use Dedoc\Scramble\Support\Type\UnknownType;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\Index;
use Doctrine\DBAL\Types\DecimalType;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionMethod;
use SplFileObject;
use UnitEnum;

/**
* All the code here was written by the great Laravel team and community. Cudos to them.
*/
class ModelInfo
{
public static array $cache = [];
Expand All @@ -43,11 +45,9 @@ class ModelInfo
'morphedByMany',
];

private string $class;

public function __construct(string $class)
{
$this->class = $class;
public function __construct(
private string $class
) {
}

public function handle()
Expand All @@ -71,10 +71,6 @@ public function type()
return static::$cache[$this->class];
}

if (! class_exists(Column::class)) {
throw new \LogicException('`doctrine/dbal` package is not installed. It is needed to get model attribute types.');
}

$modelInfo = $this->handle();

/** @var Model $model */
Expand All @@ -88,8 +84,8 @@ public function type()
? Union::wrap([new NullType(), $t])
: $t;

$type = explode(' ', $value['type']);
$typeName = explode('(', $type[0])[0];
$type = explode(' ', $value['type'] ?? '');
$typeName = explode('(', $type[0] ?? '')[0];

if (in_array($key, $model->getDates())) {
return $createType(new ObjectType('\\Carbon\\Carbon'));
Expand All @@ -103,8 +99,10 @@ public function type()
'double' => new FloatType(),
'decimal' => new FloatType(),
'string' => new StringType(),
'varchar' => new StringType(),
'text' => new StringType(),
'datetime' => new StringType(),
'tinyint' => new BooleanType(),
'bool' => new BooleanType(),
'boolean' => new BooleanType(),
'json' => new ArrayType(),
Expand Down Expand Up @@ -156,48 +154,61 @@ public function type()
*/
protected function getAttributes($model)
{
$schema = $model->getConnection()->getDoctrineSchemaManager();
$table = $model->getConnection()->getTablePrefix().$model->getTable();

$platform = $model->getConnection()
->getDoctrineConnection()
->getDatabasePlatform();

$platform->registerDoctrineTypeMapping('enum', 'string');
$platform->registerDoctrineTypeMapping('geometry', 'string');

$columns = $schema->listTableColumns($table);
$indexes = $schema->listTableIndexes($table);
$connection = $model->getConnection();
$schema = $connection->getSchemaBuilder();
$table = $model->getTable();
$columns = $schema->getColumns($table);
$indexes = $schema->getIndexes($table);

return collect($columns)
->values()
->map(fn (Column $column) => [
'name' => $column->getName(),
'type' => $this->getColumnType($column),
'increments' => $column->getAutoincrement(),
'nullable' => ! $column->getNotnull(),
->map(fn ($column) => [
'name' => $column['name'],
'type' => $column['type'],
'increments' => $column['auto_increment'],
'nullable' => $column['nullable'],
'default' => $this->getColumnDefault($column, $model),
'unique' => $this->columnIsUnique($column->getName(), $indexes),
'fillable' => $model->isFillable($column->getName()),
'hidden' => $this->attributeIsHidden($column->getName(), $model),
'unique' => $this->columnIsUnique($column['name'], $indexes),
'fillable' => $model->isFillable($column['name']),
'appended' => null,
'cast' => $this->getCastType($column->getName(), $model),
'hidden' => $this->attributeIsHidden($column['name'], $model),
'cast' => $this->getCastType($column['name'], $model),
])
->merge($this->getVirtualAttributes($model, $columns))
->keyBy('name');
}

private function getColumnDefault($column, Model $model)
{
$attributeDefault = $model->getAttributes()[$column['name']] ?? null;

return match (true) {
$attributeDefault instanceof BackedEnum => $attributeDefault->value,
$attributeDefault instanceof UnitEnum => $attributeDefault->name,
default => $attributeDefault ?? $column['default'],
};
}

private function columnIsUnique($column, array $indexes)
{
return collect($indexes)->contains(
fn ($index) => count($index['columns']) === 1 && $index['columns'][0] === $column && $index['unique']
);
}

/**
* Get the virtual (non-column) attributes for the given model.
*
* @param \Illuminate\Database\Eloquent\Model $model
* @param \Doctrine\DBAL\Schema\Column[] $columns
* @param array[] $columns
* @return \Illuminate\Support\Collection
*/
protected function getVirtualAttributes($model, $columns)
{
$class = new ReflectionClass($model);

$keyedColumns = collect($columns)->keyBy('name');

return collect($class->getMethods())
->reject(
fn (ReflectionMethod $method) => $method->isStatic()
Expand All @@ -213,7 +224,7 @@ protected function getVirtualAttributes($model, $columns)
return [];
}
})
->reject(fn ($cast, $name) => collect($columns)->has($name))
->reject(fn ($cast, $name) => $keyedColumns->has($name))
->map(fn ($cast, $name) => [
'name' => $name,
'type' => null,
Expand Down Expand Up @@ -327,43 +338,6 @@ protected function getCastsWithDates($model)
->merge($model->getCasts());
}

/**
* Get the type of the given column.
*
* @param \Doctrine\DBAL\Schema\Column $column
* @return string
*/
protected function getColumnType($column)
{
$name = $column->getType()->getName();

$unsigned = $column->getUnsigned() ? ' unsigned' : '';

$details = get_class($column->getType()) === DecimalType::class
? $column->getPrecision().','.$column->getScale()
: $column->getLength();

if ($details) {
return sprintf('%s(%s)%s', $name, $details, $unsigned);
}

return sprintf('%s%s', $name, $unsigned);
}

/**
* Get the default value for the given column.
*
* @param \Doctrine\DBAL\Schema\Column $column
* @param \Illuminate\Database\Eloquent\Model $model
* @return mixed|null
*/
protected function getColumnDefault($column, $model)
{
$attributeDefault = $model->getAttributes()[$column->getName()] ?? null;

return $attributeDefault ?? $column->getDefault();
}

/**
* Determine if the given attribute is hidden.
*
Expand All @@ -384,20 +358,6 @@ protected function attributeIsHidden($attribute, $model)
return false;
}

/**
* Determine if the given attribute is unique.
*
* @param string $column
* @param \Doctrine\DBAL\Schema\Index[] $indexes
* @return bool
*/
protected function columnIsUnique($column, $indexes)
{
return collect($indexes)
->filter(fn (Index $index) => count($index->getColumns()) === 1 && $index->getColumns()[0] === $column)
->contains(fn (Index $index) => $index->isUnique());
}

/**
* Qualify the given model class base name.
*
Expand Down
Loading

0 comments on commit 6bd44d3

Please sign in to comment.