diff --git a/admin/schema.org.md b/admin/schema.org.md index 61769e2e53c..feb76d1b561 100644 --- a/admin/schema.org.md +++ b/admin/schema.org.md @@ -18,7 +18,7 @@ To configure which property should be shown to represent your entity, map the pr // api/src/Entity/Person.php ... -#[ApiProperty(types: ["https://schema.org/name"])] +#[ApiProperty(iris: ["https://schema.org/name"])] private $name; ... diff --git a/core/dto.md b/core/dto.md index a3171424d8e..462a6a8ae17 100644 --- a/core/dto.md +++ b/core/dto.md @@ -11,6 +11,7 @@ Using an input, the request body will be denormalized to the input instead of yo ```php + */ final class UserResetPasswordProcessor implements ProcessorInterface { /** * @param UserResetPasswordDto $data + * + * @throws NotFoundHttpException */ - public function process($data, Operation $operation, array $uriVariables = [], array $context = []) + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User { if ('user@example.com' === $data->email) { return new User(email: $data->email, id: 1); @@ -71,6 +79,7 @@ Let's use a message that will be processed by [Symfony Messenger](https://symfon ```php + */ final class BookRepresentationProvider implements ProviderInterface { - public function provide(Operation $operation, array $uriVariables = [], array $context = []) + public function provide(Operation $operation, array $uriVariables = [], array $context = []): AnotherRepresentation { return new AnotherRepresentation(); } @@ -128,6 +142,7 @@ For returning another representation of your data in a [State Processor](./state ```php + */ final class BookRepresentationProcessor implements ProcessorInterface { /** * @param Book $data */ - public function process($data, Operation $operation, array $uriVariables = [], array $context = []) + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): AnotherRepresentation { return new AnotherRepresentation( $data->getId(), diff --git a/core/errors.md b/core/errors.md index f14c2c69d00..0539f937982 100644 --- a/core/errors.md +++ b/core/errors.md @@ -25,7 +25,7 @@ This can also be configured on an `ApiResource` or in an `HttpOperation`, for ex ## Exception status code decision -There are many ways of configuring the exception status code we recommend reading the guides on how to use an [Error Provider](/docs/guides/error-provider) or create an [Error Resource](/docs/guides/error-resource). +There are many ways of configuring the exception status code we recommend reading the guides on how to use an [Error Provider](https://api-platform.com/docs/guides/error-provider/) or create an [Error Resource](https://api-platform.com/docs/guides/error-resource/). 1. we look at `exception_to_status` and take one if there's a match 2. If your exception is a `Symfony\Component\HttpKernel\Exception\HttpExceptionInterface` we get its status. diff --git a/core/extensions.md b/core/extensions.md index a669f7afa0d..4f75c864e09 100644 --- a/core/extensions.md +++ b/core/extensions.md @@ -65,10 +65,12 @@ use App\Entity\Offer; use Doctrine\ORM\QueryBuilder; use Symfony\Bundle\SecurityBundle\Security; -final class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface +final readonly class CurrentUserExtension implements QueryCollectionExtensionInterface, QueryItemExtensionInterface { - public function __construct(private readonly Security $security) + public function __construct( + private Security $security, + ) { } diff --git a/core/file-upload.md b/core/file-upload.md index 2e62b897296..b0b30e57983 100644 --- a/core/file-upload.md +++ b/core/file-upload.md @@ -169,43 +169,53 @@ A [normalizer](serialization.md#normalization) could be used to set the `content ```php contentUrl = $this->storage->resolveUri($object, 'file'); + $object->contentUrl = $this->storage->resolveUri($object, 'file'); - return $this->normalizer->normalize($object, $format, $context); - } + return $this->normalizer->normalize($object, $format, $context); + } - public function supportsNormalization($data, ?string $format = null, array $context = []): bool - { - if (isset($context[self::ALREADY_CALLED])) { - return false; - } + public function supportsNormalization($data, ?string $format = null, array $context = []): bool + { - return $data instanceof MediaObject; + if (isset($context[self::ALREADY_CALLED])) { + return false; } + + return $data instanceof MediaObject; + } + + public function getSupportedTypes(?string $format): array + { + return [ + MediaObject::class => true, + ]; + } } + ``` ### Making a Request to the `/media_objects` Endpoint @@ -234,8 +244,8 @@ You will need to modify your `Caddyfile` to allow the above `contentUrl` to be a @pwa expression `( header({'Accept': '*text/html*'}) && !path( -- '/docs*', '/graphql*', '/bundles*', '/media*', '/contexts*', '/_profiler*', '/_wdt*', -+ '/media*', '/docs*', '/graphql*', '/bundles*', '/media*', '/contexts*', '/_profiler*', '/_wdt*', +- '/docs*', '/graphql*', '/bundles*', '/contexts*', '/_profiler*', '/_wdt*', ++ '/media*', '/docs*', '/graphql*', '/bundles*', '/contexts*', '/_profiler*', '/_wdt*', '*.json*', '*.html', '*.csv', '*.yml', '*.yaml', '*.xml' ) ) @@ -313,25 +323,26 @@ class MediaObjectTest extends ApiTestCase public function testCreateAMediaObject(): void { - $file = new UploadedFile('fixtures/files/image.png', 'image.png'); + // The file "image.jpg" is the folder fixtures which is in the project dir + $file = new UploadedFile(__DIR__ . '/../fixtures/image.jpg', 'image.jpg'); $client = self::createClient(); - $client->request('POST', '/media_objects', [ - 'headers' => ['Content-Type' => 'multipart/form-data'], - 'extra' => [ - // If you have additional fields in your MediaObject entity, use the parameters. - 'parameters' => [ - 'title' => 'My file uploaded', - ], - 'files' => [ - 'file' => $file, - ], - ] + $client->request('POST', 'http://localhost:8888/api/media_objects', [ + 'headers' => ['Content-Type' => 'multipart/form-data'], + 'extra' => [ + // If you have additional fields in your MediaObject entity, use the parameters. + 'parameters' => [ + // 'title' => 'title' + ], + 'files' => [ + 'file' => $file, + ], + ] ]); $this->assertResponseIsSuccessful(); $this->assertMatchesResourceItemJsonSchema(MediaObject::class); $this->assertJsonContains([ - 'title' => 'My file uploaded', + // 'title' => 'My file uploaded', ]); } } @@ -451,19 +462,28 @@ We also need to make sure the field containing the uploaded file is not denormal namespace App\Serializer; -use Symfony\Component\HttpFoundation\File\UploadedFile; +use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; final class UploadedFileDenormalizer implements DenormalizerInterface { - public function denormalize($data, string $type, string $format = null, array $context = []): UploadedFile + public function denormalize($data, string $type, string $format = null, array $context = []): File { return $data; } - public function supportsDenormalization($data, $type, $format = null): bool + public function supportsDenormalization($data, $type, $format = null, array $context = []): bool + { + return $data instanceof File; + } + + public function getSupportedTypes(?string $format): array { - return $data instanceof UploadedFile; + return [ + 'object' => null, + '*' => false, + File::class => true, + ]; } } ``` diff --git a/core/graphql.md b/core/graphql.md index 380473b0fb4..6255ca4b225 100644 --- a/core/graphql.md +++ b/core/graphql.md @@ -318,7 +318,7 @@ Now that your resolver is created and registered, you can configure your custom In your resource, add the following: -[codeselector] + ```php ```php ```php ```php ```php ```php ```php ```php ```php ```php ```php ```php -[codeselector] + ```php ```php ```php ```php + */ final class PersonProvider implements ProviderInterface { - public function provide(Operation $operation, array $uriVariables = [], array $context = []) + public function provide(Operation $operation, array $uriVariables = [], array $context = []): Person { // Our identifier is: // $uriVariables['code'] @@ -105,7 +109,8 @@ final class UuidUriVariableTransformer implements UriVariableTransformerInterfac * * @throws InvalidUriVariableException Occurs when the uriVariable could not be transformed */ - public function transform($value, array $types, array $context = []) { + public function transform($value, array $types, array $context = []): Uuid + { try { return Uuid::fromString($value); } catch (InvalidUuidStringException $e) { @@ -138,6 +143,8 @@ Tag this service as an `api_platform.uri_variables.transformer`: ```yaml +# api/config/services.yaml + services: App\Identifier\UuidUriVariableTransformer: tags: @@ -161,6 +168,7 @@ If your resource is also a Doctrine entity and you want to use another identifie ```php $PHP_INI_DIR/conf.d/blackfire.ini + && tar zxpf /tmp/blackfire-probe.tar.gz -C /tmp/blackfire \ + && mv /tmp/blackfire/blackfire-*.so $(php -r "echo ini_get ('extension_dir');")/blackfire.so \ + && printf "extension=blackfire.so\nblackfire.agent_socket=tcp://blackfire:8307\n" > $PHP_INI_DIR/conf.d/blackfire.ini \ + && rm -rf /tmp/blackfire /tmp/blackfire-probe.tar.gz ``` 4. Rebuild and restart all your containers diff --git a/core/serialization.md b/core/serialization.md index 7a22f360f3a..20da77f4f2d 100644 --- a/core/serialization.md +++ b/core/serialization.md @@ -911,11 +911,11 @@ Here is an example: namespace App\Serializer; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; -use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\NormalizerAwareTrait; +use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -class BookAttributeNormalizer implements ContextAwareNormalizerInterface, NormalizerAwareInterface +class BookAttributeNormalizer implements NormalizerInterface, NormalizerAwareInterface { use NormalizerAwareTrait; diff --git a/core/state-processors.md b/core/state-processors.md index 138c5fae0ad..c8c6e5277fc 100644 --- a/core/state-processors.md +++ b/core/state-processors.md @@ -41,9 +41,12 @@ use App\Entity\BlogPost; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProcessorInterface; -class BlogPostProcessor implements ProcessorInterface +/** + * @implements ProcessorInterface + */ +final class BlogPostProcessor implements ProcessorInterface { - public function process($data, Operation $operation, array $uriVariables = [], array $context = []) + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): BlogPost|void { // call your persistence layer to save $data return $data; @@ -51,6 +54,9 @@ class BlogPostProcessor implements ProcessorInterface } ``` +The `process()` method must return the created or modified object, or nothing (that's why `void` is allowed) for `DELETE` operations. +The `process()` method can also take an object as input, in the `$data` parameter, that isn't of the same type that its output (the returned object). See [the DTO documentation entry](dto.md) for more details. + We then configure our operation to use this processor: ```php @@ -66,32 +72,15 @@ use App\State\BlogPostProcessor; class BlogPost {} ``` -If service autowiring and autoconfiguration are enabled (they are by default), you are done! - -Otherwise, if you use a custom dependency injection configuration, you need to register the corresponding service and add the -`api_platform.state_processor` tag. - -```yaml -# api/config/services.yaml - -services: - # ... - App\State\BlogPostProcessor: ~ - # Uncomment only if autoconfiguration is disabled - #tags: [ 'api_platform.state_processor' ] -``` - ## Hooking into the Built-In State Processors -If you want to execute custom business logic before or after persistence, this can be achieved by [decorating](https://symfony.com/doc/current/service_container/service_decoration.html) the built-in state processors or using [composition](https://en.wikipedia.org/wiki/Object_composition). +If you want to execute custom business logic before or after persistence, this can be achieved by using [composition](https://en.wikipedia.org/wiki/Object_composition). -The next example uses [Symfony Mailer](https://symfony.com/doc/current/mailer.html). Read its documentation if you want to use it. - -Here is an implementation example which sends new users a welcome email after a REST `POST` or GraphQL `create` operation, in a project using the native Doctrine ORM state processor: +Here is an implementation example which uses [Symfony Mailer](https://symfony.com/doc/current/mailer.html) to send new users a welcome email after a REST `POST` or GraphQL `create` operation, in a project using the native Doctrine ORM state processor: ```php + */ final class UserProcessor implements ProcessorInterface { - public function __construct(private ProcessorInterface $persistProcessor, private ProcessorInterface $removeProcessor, MailerInterface $mailer) + public function __construct( + #[Autowire(service: 'api_platform.doctrine.orm.state.persist_processor')] + private ProcessorInterface $persistProcessor, + #[Autowire(service: 'api_platform.doctrine.orm.state.remove_processor')] + private ProcessorInterface $removeProcessor, + private MailerInterface $mailer, + ) { } - public function process($data, Operation $operation, array $uriVariables = [], array $context = []) + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User|void { if ($operation instanceof DeleteOperationInterface) { return $this->removeProcessor->process($data, $operation, $uriVariables, $context); @@ -115,10 +114,11 @@ final class UserProcessor implements ProcessorInterface $result = $this->persistProcessor->process($data, $operation, $uriVariables, $context); $this->sendWelcomeEmail($data); + return $result; } - private function sendWelcomeEmail(User $user) + private function sendWelcomeEmail(User $user): void { // Your welcome email logic... // $this->mailer->send(...); @@ -126,26 +126,11 @@ final class UserProcessor implements ProcessorInterface } ``` -Even with service autowiring and autoconfiguration enabled, you must still configure the decoration: - -```yaml -# api/config/services.yaml +The `Autowire` attribute is used to inject the built-in processor services registered by API Platform. -services: - # ... - App\State\UserProcessor: - bind: - $persistProcessor: '@api_platform.doctrine.orm.state.persist_processor' - $removeProcessor: '@api_platform.doctrine.orm.state.remove_processor' - # If you're using Doctrine MongoDB ODM, you can use the following code: - # $persistProcessor: '@api_platform.doctrine_mongodb.odm.state.persist_processor' - # $removeProcessor: '@api_platform.doctrine_mongodb.odm.state.remove_processor' - # Uncomment only if autoconfiguration is disabled - #arguments: ['@App\State\UserProcessor.inner'] - #tags: [ 'api_platform.state_processor' ] -``` +If you're using Doctrine MongoDB ODM instead of Doctrine ORM, replace `orm` by `odm` in the name of the injected services. -And configure that you want to use this processor on the User resource: +Finally, configure that you want to use this processor on the User resource: ```php + */ final class BlogPostProvider implements ProviderInterface { - public function provide(Operation $operation, array $uriVariables = [], array $context = []) + private const DATA = [ + 'ab' => new BlogPost('ab'), + 'cd' => new BlogPost('cd'), + ]; + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): BlogPost|null { - return new BlogPost($uriVariables['id']); + return self::DATA[$uriVariables['id']] ?? null; } } ``` -As this operation expects a BlogPost we return an instance of the BlogPost in the `provide` method. -The `uriVariables` parameter is an array with the values of the URI variables. +For the example, we store the list of our blog posts in an associative array (the `BlogPostProvider::DATA` constant). + +As this operation expects a `BlogPost`, the `provide` methods return the instance of the `BlogPost` corresponding to the ID passed in the URL. If the ID doesn't exist in the associative array, `provide()` returns `null`. API Platform will automatically generate a 404 response if the provider returns `null`. + +The `$uriVariables` parameter contains an array with the values of the URI variables. To use this provider we need to configure the provider on the operation: @@ -68,20 +79,6 @@ use App\State\BlogPostProvider; class BlogPost {} ``` -If you use the default configuration, the corresponding service will be automatically registered thanks to -[autowiring](https://symfony.com/doc/current/service_container/autowiring.html). -To declare the service explicitly, you can use the following snippet: - -```yaml -# api/config/services.yaml - -services: - # ... - App\State\BlogPostProvider: ~ - # Uncomment only if autoconfiguration is disabled - #tags: [ 'api_platform.state_provider' ] -``` - Now let's say that we also want to handle the `/blog_posts` URI which returns a collection. We can change the Provider into supporting a wider range of operations. Then we can provide a collection of blog posts when the operation is a `CollectionOperationInterface`: @@ -96,15 +93,23 @@ use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; use ApiPlatform\Metadata\CollectionOperationInterface; +/** + * @implements ProviderInterface + */ final class BlogPostProvider implements ProviderInterface { - public function provide(@Operation $operation, array $uriVariables = [], array $context = []) + private const DATA = [ + 'ab' => new BlogPost('ab'), + 'cd' => new BlogPost('cd'), + ]; + + public function provide(Operation $operation, array $uriVariables = [], array $context = []): iterable|BlogPost|null { if ($operation instanceof CollectionOperationInterface) { - return [new BlogPost(), new BlogPost()]; + return self::DATA; } - return new BlogPost($uriVariables['id']); + return self::DATA[$uriVariables['id']] ?? null; } } ``` @@ -140,14 +145,21 @@ use App\Dto\AnotherRepresentation; use App\Model\Book; use ApiPlatform\Metadata\Operation; use ApiPlatform\State\ProviderInterface; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +/** + * @implements ProviderInterface + */ final class BookRepresentationProvider implements ProviderInterface { - public function __construct(private ProviderInterface $itemProvider) + public function __construct( + #[Autowire(service: 'api_platform.doctrine.orm.state.item_provider')] + private ProviderInterface $itemProvider, + ) { } - public function provide(Operation $operation, array $uriVariables = [], array $context = []): object|array|null + public function provide(Operation $operation, array $uriVariables = [], array $context = []): AnotherRepresentation { $book = $this->itemProvider->provide($operation, $uriVariables, $context); @@ -159,20 +171,6 @@ final class BookRepresentationProvider implements ProviderInterface } ``` -Even with service autowiring and autoconfiguration enabled, you must still configure the decoration: - -```yaml -# api/config/services.yaml - -services: - # ... - App\State\BookRepresentationProvider: - bind: - $itemProvider: '@api_platform.doctrine.orm.state.item_provider' - # Uncomment only if autoconfiguration is disabled - #tags: [ 'api_platform.state_provider' ] -``` - And configure that you want to use this provider on the Book resource: ```php @@ -188,3 +186,27 @@ use App\State\BookRepresentationProvider; #[Get(output: AnotherRepresentation::class, provider: BookRepresentationProvider::class)] class Book {} ``` + +## Registering Services Without Autowiring + +The services in the previous examples are automatically registered because +[autowiring](https://symfony.com/doc/current/service_container/autowiring.html) + and autoconfiguration are enabled by default in API Platform. +To declare the service explicitly, you can use the following snippet: + +```yaml +# api/config/services.yaml + +services: + # ... + App\State\BlogPostProvider: ~ + tags: [ 'api_platform.state_provider' ] + +# api/config/services.yaml +services: + # ... + App\State\BookRepresentationProvider: + arguments: + $itemProvider: '@api_platform.doctrine.orm.state.item_provider' + tags: [ 'api_platform.state_provider' ] +``` diff --git a/core/subresources.md b/core/subresources.md index 4723b139acf..cc3905b21f2 100644 --- a/core/subresources.md +++ b/core/subresources.md @@ -109,6 +109,7 @@ To make things work, API Platform needs information about how to retrieve the `A the `Question`, this is done by configuring the `uriVariables`: + ```php + */ +final readonly class UserPasswordHasher implements ProcessorInterface { - public function __construct(private readonly ProcessorInterface $processor, private readonly UserPasswordHasherInterface $passwordHasher) + public function __construct( + private ProcessorInterface $processor, + private UserPasswordHasherInterface $passwordHasher + ) { } - public function process($data, Operation $operation, array $uriVariables = [], array $context = []) + /** + * @param User $data + */ + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): User|void { if (!$data->getPlainPassword()) { return $this->processor->process($data, $operation, $uriVariables, $context); @@ -262,6 +271,9 @@ Then bind it to the ORM persist processor: ```yaml # api/config/services.yaml +services: + # ... + App\State\UserPasswordHasher: bind: $processor: '@api_platform.doctrine.orm.state.persist_processor' diff --git a/core/validation.md b/core/validation.md index 60145dac3dd..407ae9fd64d 100644 --- a/core/validation.md +++ b/core/validation.md @@ -208,7 +208,7 @@ use ApiPlatform\Metadata\ApiResource; use Symfony\Component\Validator\Constraints as Assert; #[ApiResource( - validationContext: ['groups' => [Book::class, 'validationGroups'] + validationContext: ['groups' => [Book::class, 'validationGroups']] )] class Book { @@ -408,15 +408,16 @@ use ApiPlatform\State\ProcessorInterface; use ApiPlatform\Validator\ValidatorInterface; use App\Entity\MyEntity; -class MyEntityRemoveProcessor implements ProcessorInterface +final readonly class MyEntityRemoveProcessor implements ProcessorInterface { public function __construct( private DoctrineRemoveProcessor $doctrineProcessor, private ValidatorInterface $validator, - ) { + ) + { } - public function process($data, Operation $operation, array $uriVariables = [], array $context = []) + public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []) { $this->validator->validate($data, ['groups' => ['deleteValidation']]); $this->doctrineProcessor->process($data, $operation, $uriVariables, $context); @@ -469,6 +470,7 @@ For example: ```php - + }/> {/* Add your routes here */} -

Not Found

} /> + } />
diff --git a/deployment/docker-compose.md b/deployment/docker-compose.md index 364161d0b7d..4b7798336d1 100644 --- a/deployment/docker-compose.md +++ b/deployment/docker-compose.md @@ -89,8 +89,10 @@ Go to `https://your-domain-name.example.com` and enjoy! Alternatively, if you don't want to expose an HTTPS server but only an HTTP one, run the following command: -```console -SERVER_NAME=:80 \ +```bash +SERVER_NAME=http://localhost \ +MERCURE_PUBLIC_URL=http://localhost/.well-known/mercure \ +TRUSTED_HOSTS='^localhost|php$' \ APP_SECRET=ChangeMe \ CADDY_MERCURE_JWT_SECRET=ChangeThisMercureHubJWTSecretKey \ docker compose -f -compose.yaml -f compose.prod.yaml up --wait @@ -114,3 +116,66 @@ As a shortcut, `private_ranges` may be configured to trust all private IP ranges + trusted_proxies private_ranges +} ``` + +## Building Next.js client locally with SSG + +When deploying API Platform with Docker Compose and you need to build a Next.js client that utilizes Static Site Generation (SSG), a specific setup is required. +This setup ensures the Next.js client can access the API at build time to generate static pages. + +### Configuration Steps + +#### 1. Adjust the compose.prod.yaml file + +Modify the pwa service to ensure network communication between the pwa and php services during the build: + +```yaml + pwa: + build: + context: ./pwa + target: prod + network: host + extra_hosts: + - php=127.0.0.1 +``` + +#### 2. Build and start the php service + +Begin by starting the php service container: + +```bash +SERVER_NAME=http://localhost \ +MERCURE_PUBLIC_URL=http://localhost/.well-known/mercure \ +TRUSTED_HOSTS='^localhost|php$' \ +APP_SECRET=!ChangeMe! \ +CADDY_MERCURE_JWT_SECRET=ChangeThisMercureHubJWTSecretKey \ +POSTGRES_PASSWORD=!ChangeMe! \ +docker compose -f compose.yaml -f compose.prod.yaml up -d --build --wait php +``` + +#### 3. Optional: Env file with create-client + +If your are using the [create-client](../create-client/nextjs.md) generator inside your Next.js client, you need to create a `.env` file in the `pwa` directory with the `NEXT_PUBLIC_ENTRYPOINT` environment variable to ensure the Next.js client knows where to find the API: + +```dotenv +NEXT_PUBLIC_ENTRYPOINT=http://php +``` + +#### 4. Build the pwa service + +```bash +docker compose -f compose.yaml -f compose.prod.yaml build pwa +``` + +#### 5. Finally, bring up the full project + +```bash +SERVER_NAME=http://localhost \ +MERCURE_PUBLIC_URL=http://localhost/.well-known/mercure \ +TRUSTED_HOSTS='^localhost|php$' \ +APP_SECRET=!ChangeMe! \ +CADDY_MERCURE_JWT_SECRET=ChangeThisMercureHubJWTSecretKey \ +POSTGRES_PASSWORD=!ChangeMe! \ +docker compose -f compose.yaml -f compose.prod.yaml up -d --wait +``` + +These steps ensure the Next.js client can statically generate pages by accessing the API during the build process. diff --git a/deployment/kubernetes.md b/deployment/kubernetes.md index 9d5e571f340..fb8255f77d4 100644 --- a/deployment/kubernetes.md +++ b/deployment/kubernetes.md @@ -44,6 +44,12 @@ docker build -t gcr.io/test-api-platform/caddy:0.1.0 -t gcr.io/test-api-platform docker build -t gcr.io/test-api-platform/pwa:0.1.0 -t gcr.io/test-api-platform/pwa:latest pwa --target prod ``` +Optional: If your pwa project use Static Site Generation (SSG) and you need to build it against the API running locally, you can build the pwa with the command below. + +```bash +docker build -t gcr.io/test-api-platform/pwa:0.1.0 -t gcr.io/test-api-platform/pwa:latest pwa --target prod --network=host --add-host php=127.0.0.1 +``` + #### 2. Push your images to your Docker registry ```console @@ -53,12 +59,12 @@ docker push gcr.io/test-api-platform/caddy docker push gcr.io/test-api-platform/pwa ``` -Optional push the version images: +Optional: push the version images: ```console -docker push gcr.io/test-api-platform/php:0.1.0 -docker push gcr.io/test-api-platform/caddy:0.1.0 -docker push gcr.io/test-api-platform/pwa:0.1.0 +docker push gcr.io/test-api-platform/php:0.1.0 +docker push gcr.io/test-api-platform/caddy:0.1.0 +docker push gcr.io/test-api-platform/pwa:0.1.0 ``` The result should look similar to these images. @@ -114,7 +120,7 @@ Replace the values with the image parameters from the stage above. The parameter `php.appSecret` is the `AppSecret` from ./.env Fill the rest of the values with the correct settings. For available options see /helm/api-platform/values.yaml. -If you want a test deploy you can set corsAllowOrigin='*' +If you want a test deploy you can set corsAllowOrigin='\*' After a successful installation, there is a message at the end. You can copy these commands and execute them to set a port-forwarding and @@ -166,7 +172,7 @@ You can upgrade with the same command from the installation and pass all paramet ### 2. Use :latest tags Infos about [best practices for tagging images for Kubernetes](https://kubernetes.io/docs/concepts/containers/images/) -You have to use the *.image.pullPolicy=Always see the last 3 parameters. +You have to use the \*.image.pullPolicy=Always see the last 3 parameters. ```console PHP_POD=$(kubectl --namespace=bar get pods -l app=php -o jsonpath="{.items[0].metadata.name}") @@ -219,10 +225,31 @@ commandArgs: ['messenger:consume', 'async', '--memory-limit=100M'] The `readinessProbe` and the `livenessProble` can not use the default `docker-healthcheck` but should test if the command is running. +First, make sure to install the `/bin/ps` binary, otherwise the `readinessProbe` and `livenessProbe` will fail: + + + +```patch +# api/Dockerfile + +RUN apt-get update && apt-get install --no-install-recommends -y \ ++ procps \ +# ... +``` + + + +Then, update the probes: + ```yaml readinessProbe: exec: command: ["/bin/sh", "-c", "/bin/ps -ef | grep messenger:consume | grep -v grep"] - initialDelaySeconds: 120 - periodSeconds: 3 + initialDelaySeconds: 120 + periodSeconds: 3 +livenessProbe: + exec: + command: ["/bin/sh", "-c", "/bin/ps -ef | grep messenger:consume | grep -v grep"] + initialDelaySeconds: 120 + periodSeconds: 3 ``` diff --git a/distribution/caddy.md b/distribution/caddy.md index b6fa7a22f69..af0e3da931a 100644 --- a/distribution/caddy.md +++ b/distribution/caddy.md @@ -19,7 +19,7 @@ For instance, when you create your own Symfony controllers serving HTML pages, or when using bundles such as EasyAdmin or SonataAdmin. To do so, you have to tweak the rules used to route the requests. -Open `api-platform/api/docker/caddy/Caddyfile` and modify the expression. +Open `api-platform/api/frankenphp/Caddyfile` and modify the expression. You can use [any CEL (Common Expression Language) expression](https://caddyserver.com/docs/caddyfile/matchers#expression) supported by Caddy. For instance, if you want to route all requests to a path starting with `/admin` to the API, modify the existing expression like this: diff --git a/distribution/debugging.md b/distribution/debugging.md index 728137aa573..0575a303ed2 100644 --- a/distribution/debugging.md +++ b/distribution/debugging.md @@ -4,31 +4,43 @@ ## Xdebug -The default Docker stack is shipped without a Xdebug stage. It's easy -though to add [Xdebug](https://xdebug.org/) to your project, for development -purposes such as debugging tests or remote API requests. +For development purposes such as debugging tests or remote API requests, +[Xdebug](https://xdebug.org/) is shipped by default with the API Platform distribution. -## Add a Development Stage to the Dockerfile +To enable it, run: -To avoid deploying API Platform to production with an active Xdebug extension, -it's recommended to add a custom stage to the end of the `api/Dockerfile`. +```console +XDEBUG_MODE=debug docker compose up --wait +``` -```Dockerfile -# api/Dockerfile -FROM api_platform_php as api_platform_php_dev +## Using Xdebug with PhpStorm -ARG XDEBUG_VERSION=3.1.3 -RUN set -eux; \ - apk add --no-cache --virtual .build-deps $PHPIZE_DEPS; \ - pecl install xdebug-$XDEBUG_VERSION; \ - docker-php-ext-enable xdebug; \ - apk del .build-deps -``` +First, [create a PHP debug remote server configuration](https://www.jetbrains.com/help/phpstorm/creating-a-php-debug-server-configuration.html): + +1. In the `Settings/Preferences` dialog, go to `PHP | Servers` +2. Create a new server: + * Name: `api` (or whatever you want to use for the variable `PHP_IDE_CONFIG`) + * Host: `localhost` (or the one defined using the `SERVER_NAME` environment variable) + * Port: `443` + * Debugger: `Xdebug` + * Check `Use path mappings` + * Map the local `api/` directory to the `/app` absolute path on the server + +You can now use the debugger! + +1. In PhpStorm, open the `Run` menu and click on `Start Listening for PHP Debug Connections` +2. Add the `XDEBUG_SESSION=PHPSTORM` query parameter to the URL of the page you want to debug or use [other available triggers](https://xdebug.org/docs/step_debug#activate_debugger). + Alternatively, you can use [the Xdebug extension](https://xdebug.org/docs/step_debug#browser-extensions) for your preferred web browser. + +3. On the command-line, we might need to tell PhpStorm which [path mapping configuration](https://www.jetbrains.com/help/phpstorm/zero-configuration-debugging-cli.html#configure-path-mappings) should be used, set the value of the PHP_IDE_CONFIG environment variable to `serverName=api`, where `api` is the name of the debug server configured higher. + + Example: -## Configure Xdebug with Docker Compose Override + ```console + XDEBUG_SESSION=1 PHP_IDE_CONFIG="serverName=api" php bin/console ... + ``` -XDebug is shipped by default with the API Platform distribution. -To enable it, run: `XDEBUG_MODE=debug docker compose up --wait` +## Using Xdebug With VSCode If you are using VSCode, use the following `launch.json` to debug. Note that this configuration includes the path mappings for the Docker image. @@ -51,8 +63,10 @@ Note that this configuration includes the path mappings for the Docker image. } ``` -Note: On Linux, the `client_host` setting of `host.docker.internal` may not work. -In this case you will need the actual local IP address of your computer. +> [!NOTE] +> +> On Linux, the `client_host` setting of `host.docker.internal` may not work. +> In this case you will need the actual local IP address of your computer. ## Troubleshooting @@ -64,6 +78,6 @@ $ docker compose exec php \ php --version PHP … - with Xdebug v3.1.3, Copyright (c) 2002-2021, by Derick Rethans + with Xdebug v…, Copyright (c) 2002-2021, by Derick Rethans … ``` diff --git a/distribution/index.md b/distribution/index.md index 4b4e16912cf..c91cac6a5e7 100644 --- a/distribution/index.md +++ b/distribution/index.md @@ -201,7 +201,7 @@ You'll need to add a security exception in your browser to accept the self-signe for this container when installing the framework. Later you will probably replace this welcome screen by the homepage of your Next.js application. If you don't plan to create -a Progressive Web App, you can remove the `pwa/` directory as well as the related lines in `docker-compose*.yml` and in `api/docker/caddy/Caddyfile` (don't do it +a Progressive Web App, you can remove the `pwa/` directory as well as the related lines in `docker-compose*.yml` and in `api/frankenphp/Caddyfile` (don't do it now, we'll use this container later in this tutorial). Click on the "API" button, or go to `https://localhost/docs/`: diff --git a/distribution/testing.md b/distribution/testing.md index 78dc2bbc86c..51a00f44421 100644 --- a/distribution/testing.md +++ b/distribution/testing.md @@ -372,7 +372,6 @@ The API Platform Demo [contains a CD workflow](https://github.com/api-platform/d You may also be interested in these alternative testing tools (not included in the API Platform distribution): * [Hoppscotch](https://docs.hoppscotch.io/features/tests), create functional test for your API -* [Foundry](https://github.com/zenstruck/foundry), a modern fixtures library that will replace Alice as the recommended fixtures library soon; * [Hoppscotch](https://docs.hoppscotch.io/documentation/features/rest-api-testing/), create functional test for your API Platform project using a nice UI, benefit from its Swagger integration and run tests in the CI using [the command-line tool](https://docs.hoppscotch.io/cli); * [Behat](https://behat.org), a