Skip to content

Commit

Permalink
feat(laravel): test app
Browse files Browse the repository at this point in the history
  • Loading branch information
romainallanot authored and soyuka committed Jun 18, 2024
1 parent 31f3501 commit aab48a6
Show file tree
Hide file tree
Showing 21 changed files with 436 additions and 54 deletions.
7 changes: 7 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,13 @@
"friends-of-behat/mink-extension": "^2.2",
"friends-of-behat/symfony-extension": "^2.1",
"guzzlehttp/guzzle": "^6.0 || ^7.0",
"illuminate/config": "^8.70|^9.0|^10.0",
"illuminate/contracts": "^8.70|^9.0|^10.0",
"illuminate/database": "^8.70|^9.0|^10.0",
"illuminate/http": "^8.70|^9.0|^10.0",
"illuminate/pagination": "^8.70|^9.0|^10.0",
"illuminate/routing": "^8.70|^9.0|^10.0",
"illuminate/support": "^8.70|^9.0|^10.0",
"jangregor/phpstan-prophecy": "^1.0",
"justinrainbow/json-schema": "^5.2.1",
"phpspec/prophecy-phpunit": "^2.0",
Expand Down
73 changes: 34 additions & 39 deletions src/Laravel/ApiPlatformProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@
use ApiPlatform\State\Provider\DeserializeProvider;
use ApiPlatform\State\Provider\ReadProvider;
use ApiPlatform\State\ProviderInterface;
use Illuminate\Foundation\Application;
use Illuminate\Config\Repository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Foundation\CachesRoutes;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Illuminate\Routing\RouteCollection;
Expand Down Expand Up @@ -138,21 +140,10 @@ public function register(): void
{
$this->mergeConfigFrom(__DIR__.'/config/api-platform.php', 'api-platform');

// $debug = config('debug') ?? false;
/** @var Repository $config */
$config = $this->app['config'];

$defaultContext = [];
$patchFormats = config('api-platform.patch_formats') ?? ['json' => ['application/merge-patch+json']];
$formats = config('api-platform.formats') ?? ['jsonld' => ['application/ld+json']];
$docsFormats = config('api-platform.docs_formats') ?? [
'jsonopenapi' => ['application/vnd.openapi+json'],
'json' => ['application/json'],
'jsonld' => ['application/ld+json'],
'html' => ['text/html'],
];
$errorFormats = config('api-platform.error_formats') ?? [
'jsonproblem' => ['application/problem+json'],
];
$pagination = config('api-platform.collection.pagination');
$graphqlPagination = [];

$this->app->singleton(PropertyInfoExtractorInterface::class, function () {
$phpDocExtractor = class_exists(DocBlockFactory::class) ? new PhpDocExtractor() : null;
Expand All @@ -175,8 +166,8 @@ public function register(): void

$this->app->bind(PathSegmentNameGeneratorInterface::class, UnderscorePathSegmentNameGenerator::class);

$this->app->singleton(ResourceNameCollectionFactoryInterface::class, function () {
$paths = config('api-platform.resources') ?? [];
$this->app->singleton(ResourceNameCollectionFactoryInterface::class, function () use ($config) {
$paths = $config->get('api-platform.resources') ?? [];
$refl = new \ReflectionClass(Error::class);
$paths[] = \dirname($refl->getFileName());

Expand Down Expand Up @@ -215,7 +206,7 @@ public function register(): void
});

// TODO: add cached metadata factories
$this->app->singleton(ResourceMetadataCollectionFactoryInterface::class, function (Application $app) use ($formats, $patchFormats) {
$this->app->singleton(ResourceMetadataCollectionFactoryInterface::class, function (Application $app) use ($config) {
return new EloquentResourceCollectionMetadataFactory(new AlternateUriResourceMetadataCollectionFactory(
new FiltersResourceMetadataCollectionFactory(
new FormatsResourceMetadataCollectionFactory(
Expand All @@ -230,15 +221,15 @@ public function register(): void
new NotExposedOperationResourceMetadataCollectionFactory(
$this->app->make(LinkFactoryInterface::class),
// TODO: graphql
new AttributesResourceMetadataCollectionFactory(null, $app->make(LoggerInterface::class), ['routePrefix' => config('api-platform.prefix') ?? '/'], false)
new AttributesResourceMetadataCollectionFactory(null, $app->make(LoggerInterface::class), ['routePrefix' => $config->get('api-platform.prefix') ?? '/'], false)
)
)
)
)
)
),
$formats,
$patchFormats,
$config->get('api-platform.formats'),
$config->get('api-platform.patch_formats'),
)
)
));
Expand All @@ -265,8 +256,8 @@ public function register(): void
$this->app->singleton(ReadProvider::class, function (Application $app) {
return new ReadProvider($app->make(CallableProvider::class));
});
$this->app->singleton(ContentNegotiationProvider::class, function (Application $app) use ($formats, $errorFormats) {
return new ContentNegotiationProvider($app->make(DeserializeProvider::class), new Negotiator(), $formats, $errorFormats);
$this->app->singleton(ContentNegotiationProvider::class, function (Application $app) use ($config) {
return new ContentNegotiationProvider($app->make(DeserializeProvider::class), new Negotiator(), $config->get('api-platform.formats'), $config->get('api-platform.error_formats'));
});

$this->app->singleton(DeserializeProvider::class, function (Application $app) {
Expand Down Expand Up @@ -384,6 +375,7 @@ public function register(): void
$app->make(ResourceClassResolverInterface::class),
$app->make(PropertyAccessorInterface::class),
$app->make(NameConverterInterface::class),
$app->make(ClassMetadataFactoryInterface::class),
$app->make(LoggerInterface::class),
$app->make(ResourceMetadataCollectionFactoryInterface::class),
/* $resourceAccessChecker */ null,
Expand Down Expand Up @@ -418,12 +410,12 @@ public function register(): void
);
});

$this->app->singleton(Options::class, function (Application $app) {
return new Options(title: config('api-platform.title') ?? '');
$this->app->singleton(Options::class, function (Application $app) use ($config) {
return new Options(title: $config->get('api-platform.title') ?? '');
});

$this->app->singleton(DocumentationAction::class, function (Application $app) use ($docsFormats) {
return new DocumentationAction($app->make(ResourceNameCollectionFactoryInterface::class), config('api-platform.title') ?? '', config('api-platform.description') ?? '', config('api-platform.version') ?? '', $app->make(OpenApiFactoryInterface::class), $app->make(ProviderInterface::class), $app->make(ProcessorInterface::class), $app->make(Negotiator::class), $docsFormats);
$this->app->singleton(DocumentationAction::class, function (Application $app) use ($config) {
return new DocumentationAction($app->make(ResourceNameCollectionFactoryInterface::class), $config->get('api-platform.title') ?? '', $config->get('api-platform.description') ?? '', $config->get('api-platform.version') ?? '', $app->make(OpenApiFactoryInterface::class), $app->make(ProviderInterface::class), $app->make(ProcessorInterface::class), $app->make(Negotiator::class), $config->get('api-platform.docs_formats'));
});

$this->app->singleton(FilterLocator::class, FilterLocator::class);
Expand All @@ -432,11 +424,13 @@ public function register(): void
return new EntrypointAction($app->make(ResourceNameCollectionFactoryInterface::class), $app->make(ProviderInterface::class), $app->make(ProcessorInterface::class), ['jsonld' => ['application/ld+json']]);
});

$this->app->singleton(Pagination::class, function () use ($pagination, $graphqlPagination) {
return new Pagination($pagination, $graphqlPagination);
$this->app->singleton(Pagination::class, function () use ($config) {
return new Pagination($config->get('api-platform.collection.pagination'), []);
});

$this->app->singleton(PaginationOptions::class, function () use ($pagination) {
$this->app->singleton(PaginationOptions::class, function () use ($config) {
$pagination = $config->get('api-platform.collection.pagination');

return new PaginationOptions(
$pagination['enabled'],
$pagination['page_parameter_name'],
Expand All @@ -453,7 +447,7 @@ public function register(): void
});

$this->app->bind(OpenApiFactoryInterface::class, OpenApiFactory::class);
$this->app->singleton(OpenApiFactory::class, function (Application $app) use ($formats) {
$this->app->singleton(OpenApiFactory::class, function (Application $app) use ($config) {
return new OpenApiFactory(
$app->make(ResourceNameCollectionFactoryInterface::class),
$app->make(ResourceMetadataCollectionFactoryInterface::class),
Expand All @@ -462,7 +456,7 @@ public function register(): void
$app->make(SchemaFactoryInterface::class),
$app->make(TypeFactoryInterface::class),
$app->make(FilterLocator::class),
$formats,
$config->get('api-platform.formats'),
null, // ?Options $openApiOptions = null,
$app->make(PaginationOptions::class), // ?PaginationOptions $paginationOptions = null,
// ?RouterInterface $router = null
Expand Down Expand Up @@ -572,11 +566,11 @@ public function boot(ResourceNameCollectionFactoryInterface $resourceNameCollect
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__.'/config/api-platform.php' => config_path('api-platform.php'),
__DIR__.'/config/api-platform.php' => $this->app->configPath('api-platform.php'),
], 'laravel-assets');

$this->publishes([
__DIR__.'/public' => public_path('vendor/api-platform'),
__DIR__.'/public' => $this->app->publicPath('vendor/api-platform'),
], 'laravel-assets');
}

Expand All @@ -603,19 +597,20 @@ public function boot(ResourceNameCollectionFactoryInterface $resourceNameCollect
}
}

$route = new Route(['GET'], (config('api-platform.prefix') ?? '').'/contexts/{shortName?}{_format?}', [ContextAction::class, '__invoke']);
$prefix = $this->app['config']->get('api-platform.prefix') ?? '';
$route = new Route(['GET'], $prefix.'/contexts/{shortName?}{_format?}', [ContextAction::class, '__invoke']);
$route->name('api_jsonld_context')->middleware(ApiPlatformMiddleware::class);
$routeCollection->add($route);
// Maybe that we can alias Symfony Request to Laravel Request within the provider ?
$route = new Route(['GET'], (config('api-platform.prefix') ?? '').'/docs{_format?}', function (Request $request, Application $app) {
$route = new Route(['GET'], $prefix.'/docs{_format?}', function (Request $request, Application $app) {
$documentationAction = $app->make(DocumentationAction::class);

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

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

return $entrypointAction->__invoke($request);
Expand All @@ -627,11 +622,11 @@ public function boot(ResourceNameCollectionFactoryInterface $resourceNameCollect

private function shouldRegisterRoutes(): bool
{
if (!config('api-platform.register_routes')) {
if (!$this->app['config']->get('api-platform.register_routes')) {
return false;
}

if ($this->app->routesAreCached()) {
if ($this->app instanceof CachesRoutes && $this->app->routesAreCached()) {
return false;
}

Expand Down
11 changes: 11 additions & 0 deletions src/Laravel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# API Platform - Laravel

## Testing

cd src/Laravel
composer install
composer serve
vendor/bin/phpunit



113 changes: 113 additions & 0 deletions src/Laravel/Tests/BookTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
<?php

declare(strict_types=1);

use Illuminate\Foundation\Testing\RefreshDatabase;
use Orchestra\Testbench\Concerns\WithWorkbench;
use Orchestra\Testbench\TestCase;
use Workbench\App\Models\Book;

class BookTest extends TestCase
{
use RefreshDatabase;
use WithWorkbench;

public function testGetCollection(): void
{
$response = $this->get('/api/books');
$response->assertStatus(200);
$response->assertHeader('content-type', 'application/ld+json; charset=utf-8');
$response->assertJsonFragment([
'@context' => '/api/contexts/Book',
'@id' => '/api/books',
'@type' => 'hydra:Collection',
'hydra:totalItems' => 10,
]);
$response->assertJsonCount(5, 'hydra:member');
}

public function testGetBook(): void
{
$book = Book::find(1);
$response = $this->get('/api/books/1');
$response->assertStatus(200);
$response->assertHeader('content-type', 'application/ld+json; charset=utf-8');
$response->assertJsonFragment([
'@context' => '/api/contexts/Book',
'@id' => '/api/books/1',
'@type' => 'Book',
'id' => 1,
'name' => $book->name,
]);
}

public function testCreateBook(): void
{
$response = $this->postJson(
'/api/books',
[
'name' => 'Don Quichotte',
],
[
'Accept' => 'application/ld+json',
'CONTENT_TYPE' => 'application/ld+json',
]
);

$response->assertStatus(201);
$response->assertHeader('content-type', 'application/ld+json; charset=utf-8');
$response->assertJsonFragment([
'@context' => '/api/contexts/Book',
'@type' => 'Book',
'name' => 'Don Quichotte',
]);
$this->assertMatchesRegularExpression('~^/api/books/\d+$~', $response->json('@id'));
}

public function testUpdateBook(): void
{
$iri = '/api/books/1';
$response = $this->putJson(
$iri,
[
'name' => 'updated title',
],
[
'Accept' => 'application/ld+json',
'CONTENT_TYPE' => 'application/ld+json',
]
);
$response->assertStatus(200);
$response->assertJsonFragment([
'@id' => $iri,
'name' => 'updated title',
]);
}

public function testPatchBook(): void
{
$iri = '/api/books/1';
$response = $this->patchJson(
$iri,
[
'name' => 'updated title',
],
[
'Accept' => 'application/ld+json',
'CONTENT_TYPE' => 'application/merge-patch+json',
]
);
$response->assertStatus(200);
$response->assertJsonFragment([
'@id' => $iri,
'name' => 'updated title',
]);
}

public function testDeleteBook(): void
{
$response = $this->delete('/api/books/1');
$response->assertStatus(204);
$this->assertNull(Book::find(1));
}
}
Loading

0 comments on commit aab48a6

Please sign in to comment.