Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: api-platform/core
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v4.0.16
Choose a base ref
...
head repository: api-platform/core
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v4.0.17
Choose a head ref
  • 12 commits
  • 30 files changed
  • 4 contributors

Commits on Jan 28, 2025

  1. test: guides migration table exist (#6931)

    soyuka authored Jan 28, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    7c52f34 View commit details
  2. Merge 3.4

    soyuka committed Jan 28, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    209b633 View commit details

Commits on Feb 3, 2025

  1. fix(laravel): SwaggerUI custom CSS (#6937)

    dunglas authored Feb 3, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    d5b48b1 View commit details
  2. fix(symfony): error wrongly inherit normalization context (#6939)

    soyuka authored Feb 3, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    5c7e0d2 View commit details

Commits on Feb 6, 2025

  1. fix: ensure template files have a tpl file extension (#6826) (#6829)

    kochen authored Feb 6, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    de2d298 View commit details
  2. fix(metadata): allow serializer attribute object in ApiProperty::$ser…

    …ialize (#6946)
    dunglas authored Feb 6, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    da796b9 View commit details

Commits on Feb 7, 2025

  1. fix(laravel): mitigate property metadata read for Error (#6951)

    soyuka authored Feb 7, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    af35d34 View commit details
  2. fix(laravel): Prevent overwriting existing routes on the router (#6941)

    * Prevent overwriting existing routes on the router
    * Move routes to its own routes file and add domain support
    * Fix tests and stan. Run cs fixer
    * Revert laravel pint changes
    jonerickson authored Feb 7, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    5124d8c View commit details
  3. chore: remove parallel configuration as it causes issues in rpc mode

    soyuka committed Feb 7, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    8981672 View commit details
  4. Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    dad1760 View commit details
  5. perf: various optimizations for Laravel/Symfony (#6954)

    soyuka authored Feb 7, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    49da8ec View commit details
  6. docs: v4.0.17

    soyuka committed Feb 7, 2025

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
    Copy the full SHA
    155c01f View commit details
Showing with 370 additions and 179 deletions.
  1. +4 −0 .github/workflows/guides.yaml
  2. +0 −1 .php-cs-fixer.dist.php
  3. +16 −0 CHANGELOG.md
  4. +1 −1 docs/pdg.config.yaml
  5. +8 −2 docs/src/Kernel.php
  6. +3 −96 src/Laravel/ApiPlatformProvider.php
  7. +1 −0 src/Laravel/ApiResource/Error.php
  8. 0 src/Laravel/Console/Maker/Resources/skeleton/{StateProcessor.tpl.php → StateProcessor.php.tpl}
  9. 0 src/Laravel/Console/Maker/Resources/skeleton/{StateProvider.tpl.php → StateProvider.php.tpl}
  10. +2 −2 src/Laravel/Console/Maker/Utils/StateTemplateGenerator.php
  11. +21 −28 src/Laravel/Eloquent/Metadata/ModelMetadata.php
  12. +48 −0 src/Laravel/Tests/ApiTest.php
  13. 0 .../Tests/Console/Maker/Resources/skeleton/{AppServiceProvider.tpl.php → AppServiceProvider.php.tpl}
  14. +1 −1 src/Laravel/Tests/Console/Maker/Utils/AppServiceFileGenerator.php
  15. +11 −10 src/Laravel/config/api-platform.php
  16. +2 −24 src/Laravel/public/style.css
  17. +80 −0 src/Laravel/routes/api.php
  18. +2 −2 src/Metadata/ApiProperty.php
  19. +7 −3 src/Metadata/Resource/Factory/ConcernsResourceNameCollectionFactory.php
  20. +17 −3 src/Metadata/Resource/Factory/LinkFactory.php
  21. +19 −2 src/Metadata/Util/ReflectionClassRecursiveIterator.php
  22. +0 −1 src/Serializer/Mapping/Loader/PropertyMetadataLoader.php
  23. +1 −1 src/Symfony/EventListener/ErrorListener.php
  24. +1 −1 src/Symfony/Maker/MakeStateProcessor.php
  25. +1 −1 src/Symfony/Maker/MakeStateProvider.php
  26. 0 src/Symfony/Maker/Resources/skeleton/{StateProcessor.tpl.php → StateProcessor.php.tpl}
  27. 0 src/Symfony/Maker/Resources/skeleton/{StateProvider.tpl.php → StateProvider.php.tpl}
  28. +46 −0 tests/Fixtures/TestBundle/ApiResource/Issue6926/Error.php
  29. +40 −0 tests/Fixtures/TestBundle/ApiResource/Issue6926/ThrowsAnExceptionWithGroup.php
  30. +38 −0 tests/Functional/Issues/Issue6926Test.php
4 changes: 4 additions & 0 deletions .github/workflows/guides.yaml
Original file line number Diff line number Diff line change
@@ -4,6 +4,10 @@ on:
push:
pull_request:

concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

env:
COMPOSER_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERAGE: '0'
1 change: 0 additions & 1 deletion .php-cs-fixer.dist.php
Original file line number Diff line number Diff line change
@@ -29,7 +29,6 @@
]);

return (new PhpCsFixer\Config())
->setParallelConfig(PhpCsFixer\Runner\Parallel\ParallelConfigFactory::detect())
->setRiskyAllowed(true)
->setRules([
'@DoctrineAnnotation' => true,
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
# Changelog

## v4.0.17

### Bug fixes

* [5124d8c57](https://github.com/api-platform/core/commit/5124d8c571bc4324aa060e4ff808d48f0ffa8d73) fix(laravel): Prevent overwriting existing routes on the router (#6941)
* [5c7e0d2c0](https://github.com/api-platform/core/commit/5c7e0d2c015b5264b2f857abc7e6cb944582de21) fix(symfony): error wrongly inherit normalization context (#6939)
* [af35d34d0](https://github.com/api-platform/core/commit/af35d34d01e62e96dd81dadde6a056ce67d47703) fix(laravel): mitigate property metadata read for Error (#6951)
* [d5b48b1cd](https://github.com/api-platform/core/commit/d5b48b1cd6163ee755211476fdd3d4dd0bf6f7ae) fix(laravel): SwaggerUI custom CSS (#6937)
* [da796b979](https://github.com/api-platform/core/commit/da796b979384663c3eaf4e4fe4d215b447800844) fix(metadata): allow serializer attribute object in ApiProperty::$serialize (#6946)
* [de2d298e3](https://github.com/api-platform/core/commit/de2d298e306b196bce834af290aec242807ea39b) fix: ensure template files have a tpl file extension (#6826) (#6829)
* [b6a67a197](https://github.com/api-platform/core/commit/b6a67a197a668ce15f216cffbcddc637f19c69c2) perf: various optimizations for Laravel/Symfony (#6954)

To save some time during cache warmup we recommend to define uri variables such as: `uriVariables: ['id']`. More details at #6954.

### Features

## v4.0.16

### Bug fixes
2 changes: 1 addition & 1 deletion docs/pdg.config.yaml
Original file line number Diff line number Diff line change
@@ -5,7 +5,7 @@ pdg:
src: './guides'
references:
base_url: '/docs/reference'
exclude: ['*Factory.php', '*.tpl.php', 'deprecation.php']
exclude: ['*Factory.php', '*.php.tpl', 'deprecation.php']
exclude_path: ['OpenApi/Tests', 'JsonSchema/Tests', 'RamseyUuid/Tests', 'Metadata/Tests', 'HttpCache/Tests', 'Elasticsearch/Tests', 'Doctrine/Common/Tests', 'GraphQl/Tests', 'Serializer/Tests']
namespace: 'ApiPlatform'
output: 'dist/reference'
10 changes: 8 additions & 2 deletions docs/src/Kernel.php
Original file line number Diff line number Diff line change
@@ -164,9 +164,15 @@ public function executeMigrations(string $direction = Direction::UP): void
$em = $this->getContainer()->get('doctrine.orm.entity_manager');
$loader = new ExistingEntityManager($em);
$dependencyFactory = DependencyFactory::fromEntityManager($confLoader, $loader);
$metadataStorage = $dependencyFactory->getMetadataStorage();

$dependencyFactory->getMetadataStorage()->ensureInitialized();
$executed = $dependencyFactory->getMetadataStorage()->getExecutedMigrations();
try {
$metadataStorage->ensureInitialized();
} catch (\Exception) {
// table exists
}

$executed = $metadataStorage->getExecutedMigrations();

if ($executed->hasMigration(new Version($migrationClass)) && Direction::DOWN !== $direction) {
continue;
99 changes: 3 additions & 96 deletions src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
@@ -66,7 +66,6 @@
use ApiPlatform\JsonApi\Serializer\ItemNormalizer as JsonApiItemNormalizer;
use ApiPlatform\JsonApi\Serializer\ObjectNormalizer as JsonApiObjectNormalizer;
use ApiPlatform\JsonApi\Serializer\ReservedAttributeNameConverter;
use ApiPlatform\JsonLd\Action\ContextAction;
use ApiPlatform\JsonLd\AnonymousContextBuilderInterface;
use ApiPlatform\JsonLd\ContextBuilder as JsonLdContextBuilder;
use ApiPlatform\JsonLd\ContextBuilderInterface;
@@ -124,7 +123,6 @@
use ApiPlatform\Laravel\State\SwaggerUiProcessor;
use ApiPlatform\Laravel\State\SwaggerUiProvider;
use ApiPlatform\Laravel\State\ValidateProvider;
use ApiPlatform\Metadata\Exception\NotExposedHttpException;
use ApiPlatform\Metadata\IdentifiersExtractor;
use ApiPlatform\Metadata\IdentifiersExtractorInterface;
use ApiPlatform\Metadata\InflectorInterface;
@@ -196,10 +194,6 @@
use Illuminate\Config\Repository as ConfigRepository;
use Illuminate\Contracts\Debug\ExceptionHandler as ExceptionHandlerInterface;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Foundation\CachesRoutes;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Illuminate\Routing\RouteCollection;
use Illuminate\Routing\Router;
use Illuminate\Support\ServiceProvider;
use Negotiation\Negotiator;
@@ -253,6 +247,7 @@ public function register(): void
);
});

$this->app->singleton(ModelMetadata::class);
$this->app->bind(LoaderInterface::class, AttributeLoader::class);
$this->app->bind(ClassMetadataFactoryInterface::class, ClassMetadataFactory::class);
$this->app->singleton(ClassMetadataFactory::class, function (Application $app) {
@@ -1346,7 +1341,7 @@ private function registerGraphQl(Application $app): void
/**
* Bootstrap services.
*/
public function boot(ResourceNameCollectionFactoryInterface $resourceNameCollectionFactory, ResourceMetadataCollectionFactoryInterface $resourceMetadataFactory, Router $router): void
public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->publishes([
@@ -1368,94 +1363,6 @@ public function boot(ResourceNameCollectionFactoryInterface $resourceNameCollect
$typeBuilder->setFieldsBuilderLocator(new ServiceLocator(['api_platform.graphql.fields_builder' => $fieldsBuilder]));
}

if (!$this->shouldRegisterRoutes()) {
return;
}

$globalMiddlewares = $config->get('api-platform.routes.middleware');
$routeCollection = new RouteCollection();
foreach ($resourceNameCollectionFactory->create() as $resourceClass) {
foreach ($resourceMetadataFactory->create($resourceClass) as $resourceMetadata) {
foreach ($resourceMetadata->getOperations() as $operation) {
$uriTemplate = $operation->getUriTemplate();
// _format is read by the middleware
$uriTemplate = $operation->getRoutePrefix().str_replace('{._format}', '{_format?}', $uriTemplate);
$route = (new Route([$operation->getMethod()], $uriTemplate, [ApiPlatformController::class, '__invoke']))
->where('_format', '^\.[a-zA-Z]+')
->name($operation->getName())
->setDefaults(['_api_operation_name' => $operation->getName(), '_api_resource_class' => $operation->getClass()]);

$route->middleware(ApiPlatformMiddleware::class.':'.$operation->getName());
$route->middleware($globalMiddlewares);
$route->middleware($operation->getMiddleware());

$routeCollection->add($route);
}
}
}

$prefix = $config->get('api-platform.defaults.route_prefix') ?? '';
$route = new Route(['GET'], $prefix.'/contexts/{shortName?}{_format?}', [ContextAction::class, '__invoke']);
$route->name('api_jsonld_context');
$route->middleware(ApiPlatformMiddleware::class);
$route->middleware($globalMiddlewares);
$routeCollection->add($route);
$route = new Route(['GET'], $prefix.'/docs{_format?}', function (Request $request, Application $app) {
$documentationAction = $app->make(DocumentationController::class);

return $documentationAction->__invoke($request);
});
$route->name('api_doc');
$route->middleware(ApiPlatformMiddleware::class);
$route->middleware($globalMiddlewares);
$routeCollection->add($route);

$route = new Route(['GET'], $prefix.'/.well-known/genid/{id}', function (): void {
throw new NotExposedHttpException('This route is not exposed on purpose. It generates an IRI for a collection resource without identifier nor item operation.');
});
$route->name('api_genid');
$route->middleware(ApiPlatformMiddleware::class);
$route->middleware($globalMiddlewares);
$routeCollection->add($route);

if ($config->get('api-platform.graphql.enabled')) {
$route = new Route(['POST', 'GET'], $prefix.'/graphql', function (Application $app, Request $request) {
$entrypointAction = $app->make(GraphQlEntrypointController::class);

return $entrypointAction->__invoke($request);
});
$route->middleware($globalMiddlewares);
$routeCollection->add($route);

$route = new Route(['GET'], $prefix.'/graphiql', function (Application $app) {
$controller = $app->make(GraphiQlController::class);

return $controller->__invoke();
});
$route->middleware($globalMiddlewares);
$routeCollection->add($route);
}

$route = new Route(['GET'], $prefix.'/{index?}{_format?}', function (Request $request, Application $app) {
$entrypointAction = $app->make(EntrypointController::class);

return $entrypointAction->__invoke($request);
});
$route->where('index', 'index');
$route->name('api_entrypoint');
$route->middleware(ApiPlatformMiddleware::class);
$route->middleware($globalMiddlewares);
$routeCollection->add($route);

$router->setRoutes($routeCollection);
}

private function shouldRegisterRoutes(): bool
{
if ($this->app instanceof CachesRoutes && $this->app->routesAreCached()) {
return false;
}

return true;
$this->loadRoutesFrom(__DIR__.'/routes/api.php');
}
}
1 change: 1 addition & 0 deletions src/Laravel/ApiResource/Error.php
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@
#[ErrorResource(
types: ['hydra:Error'],
openapi: false,
uriVariables: ['status'],
operations: [
new Operation(
name: '_api_errors_problem',
4 changes: 2 additions & 2 deletions src/Laravel/Console/Maker/Utils/StateTemplateGenerator.php
Original file line number Diff line number Diff line change
@@ -49,8 +49,8 @@ public function generate(string $pathLink, string $stateClassName, StateTypeEnum
private function loadTemplate(StateTypeEnum $stateTypeEnum): string
{
$templateFile = match ($stateTypeEnum) {
StateTypeEnum::Provider => 'StateProvider.tpl.php',
StateTypeEnum::Processor => 'StateProcessor.tpl.php',
StateTypeEnum::Provider => 'StateProvider.php.tpl',
StateTypeEnum::Processor => 'StateProcessor.php.tpl',
};

$templatePath = \dirname(__DIR__).'/Resources/skeleton/'.$templateFile;
49 changes: 21 additions & 28 deletions src/Laravel/Eloquent/Metadata/ModelMetadata.php
Original file line number Diff line number Diff line change
@@ -16,7 +16,6 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Str;

/**
@@ -26,6 +25,16 @@
*/
final class ModelMetadata
{
/**
* @var array<class-string, Collection<string, mixed>>
*/
private $attributesLocalCache = [];

/**
* @var array<class-string, Collection<int, mixed>>
*/
private $relationsLocalCache = [];

/**
* The methods that can be called in a model to indicate a relation.
*
@@ -45,31 +54,25 @@ final class ModelMetadata
'morphedByMany',
];

/**
* Gets the first policy associated with this model.
*/
public function getPolicy(Model $model): ?string
{
$policy = Gate::getPolicyFor($model::class);

return $policy ? $policy::class : null;
}

/**
* Gets the column attributes for the given model.
*
* @return Collection<string, mixed>
*/
public function getAttributes(Model $model): Collection
{
if (isset($this->attributesLocalCache[$model::class])) {
return $this->attributesLocalCache[$model::class];
}

$connection = $model->getConnection();
$schema = $connection->getSchemaBuilder();
$table = $model->getTable();
$columns = $schema->getColumns($table);
$indexes = $schema->getIndexes($table);
$relations = $this->getRelations($model);

return collect($columns)
return $this->attributesLocalCache[$model::class] = collect($columns)
->reject(
fn ($column) => $relations->contains(
fn ($relation) => $relation['foreign_key'] === $column['name']
@@ -112,7 +115,7 @@ private function isColumnPrimaryKey(array $indexes, string $column): bool
*
* @return Collection<int, mixed>
*/
public function getVirtualAttributes(Model $model, array $columns): Collection
private function getVirtualAttributes(Model $model, array $columns): Collection
{
$class = new \ReflectionClass($model);

@@ -155,7 +158,11 @@ public function getVirtualAttributes(Model $model, array $columns): Collection
*/
public function getRelations(Model $model): Collection
{
return collect(get_class_methods($model))
if (isset($this->relationsLocalCache[$model::class])) {
return $this->relationsLocalCache[$model::class];
}

return $this->relationsLocalCache[$model::class] = collect(get_class_methods($model))
->map(fn ($method) => new \ReflectionMethod($model, $method))
->reject(
fn (\ReflectionMethod $method) => $method->isStatic()
@@ -207,20 +214,6 @@ public function getRelations(Model $model): Collection
->values();
}

/**
* Gets the Events that the model dispatches.
*
* @return Collection<int, mixed>
*/
public function getEvents(Model $model): Collection
{
return collect($model->dispatchesEvents())
->map(fn (string $class, string $event) => [
'event' => $event,
'class' => $class,
])->values();
}

/**
* Gets the cast type for the given column.
*/
48 changes: 48 additions & 0 deletions src/Laravel/Tests/ApiTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
<?php

/*
* This file is part of the API Platform project.
*
* (c) Kévin Dunglas <dunglas@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

use ApiPlatform\Laravel\Test\ApiTestAssertionsTrait;
use Illuminate\Contracts\Config\Repository;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Orchestra\Testbench\Concerns\WithWorkbench;
use Orchestra\Testbench\TestCase;

class ApiTest extends TestCase
{
use ApiTestAssertionsTrait;
use RefreshDatabase;
use WithWorkbench;

/**
* @param Application $app
*/
protected function defineEnvironment($app): void
{
tap($app['config'], function (Repository $config): void {
$config->set('api-platform.routes.domain', 'http://test.com');
$config->set('app.debug', true);
$config->set('api-platform.formats', ['jsonld' => ['application/ld+json']]);
$config->set('api-platform.docs_formats', ['jsonld' => ['application/ld+json']]);
});
}

public function testDomainCanBeSet(): void
{
$response = $this->get('http://foobar.com/api/', ['accept' => ['application/ld+json']]);
$response->assertNotFound();

$response = $this->get('http://test.com/api/', ['accept' => ['application/ld+json']]);
$response->assertSuccessful();
}
}
Original file line number Diff line number Diff line change
@@ -27,7 +27,7 @@ public function __construct(private Filesystem $filesystem)
*/
public function regenerateProviderFile(): void
{
$templatePath = \dirname(__DIR__).'/Resources/skeleton/AppServiceProvider.tpl.php';
$templatePath = \dirname(__DIR__).'/Resources/skeleton/AppServiceProvider.php.tpl';
$targetPath = base_path('app/Providers/AppServiceProvider.php');

$this->regenerateFileFromTemplate($templatePath, $targetPath);
Loading