diff --git a/docs/Makefile b/docs/Makefile index 83092b2ff4e..6b74cd2a562 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -14,9 +14,6 @@ index: references: ./bin/pdg pdg:references -configuration-reference: - ./bin/pdg pdg:generate-configuration pages/reference/Configuration.mdx - clean: rm -r pages/reference/* rm pages/guide/*.mdx diff --git a/docs/explanation/metadata.md b/docs/explanation/metadata.md deleted file mode 100644 index dc7c9e7ee5a..00000000000 --- a/docs/explanation/metadata.md +++ /dev/null @@ -1,28 +0,0 @@ -# Metadata - -explain metadata system, metadata factories - -referenced in ApiResource.php - -## Open Vocabulary Generated from Validation Metadata - -API Platform automatically detects Symfony's built-in validators and generates schema.org IRI metadata accordingly. This allows for rich clients such as the Admin component to infer the field types for most basic use cases. - -The following validation constraints are covered: - -Constraints | Vocabulary | ---------------------------------------------------------------------------------------|-----------------------------------| -[`Url`](https://symfony.com/doc/current/reference/constraints/Url.html) | `https://schema.org/url` | -[`Email`](https://symfony.com/doc/current/reference/constraints/Email.html) | `https://schema.org/email` | -[`Uuid`](https://symfony.com/doc/current/reference/constraints/Uuid.html) | `https://schema.org/identifier` | -[`CardScheme`](https://symfony.com/doc/current/reference/constraints/CardScheme.html) | `https://schema.org/identifier` | -[`Bic`](https://symfony.com/doc/current/reference/constraints/Bic.html) | `https://schema.org/identifier` | -[`Iban`](https://symfony.com/doc/current/reference/constraints/Iban.html) | `https://schema.org/identifier` | -[`Date`](https://symfony.com/doc/current/reference/constraints/Date.html) | `https://schema.org/Date` | -[`DateTime`](https://symfony.com/doc/current/reference/constraints/DateTime.html) | `https://schema.org/DateTime` | -[`Time`](https://symfony.com/doc/current/reference/constraints/Time.html) | `https://schema.org/Time` | -[`Image`](https://symfony.com/doc/current/reference/constraints/Image.html) | `https://schema.org/image` | -[`File`](https://symfony.com/doc/current/reference/constraints/File.html) | `https://schema.org/MediaObject` | -[`Currency`](https://symfony.com/doc/current/reference/constraints/Currency.html) | `https://schema.org/priceCurrency` | -[`Isbn`](https://symfony.com/doc/current/reference/constraints/Isbn.html) | `https://schema.org/isbn` | -[`Issn`](https://symfony.com/doc/current/reference/constraints/Issn.html) | `https://schema.org/issn` | diff --git a/docs/guide/000-Declare-a-Resource.php b/docs/guide/000-Declare-a-Resource.php index 8a7e640df7f..0deefa9259e 100644 --- a/docs/guide/000-Declare-a-Resource.php +++ b/docs/guide/000-Declare-a-Resource.php @@ -3,6 +3,7 @@ // slug: declare-a-resource // name: Declare a Resource // position: 1 +// executable: true // --- // # Declare a Resource diff --git a/docs/guide/001-Provide-the-Resource-state.php b/docs/guide/001-Provide-the-Resource-state.php index fc60229d5d4..abdc6626536 100644 --- a/docs/guide/001-Provide-the-Resource-state.php +++ b/docs/guide/001-Provide-the-Resource-state.php @@ -3,6 +3,7 @@ // slug: provide-the-resource-state // name: Provide the Resource State // position: 2 +// executable: true // --- // # Provide the Resource State @@ -12,7 +13,7 @@ use ApiPlatform\Metadata\ApiResource; use App\State\BookProvider; - // We use a `BookProvider` as the [ApiResource::provider](http://localhost:3000/reference/Metadata/ApiResource#provider) option. + // We use a `BookProvider` as the [ApiResource::provider](/reference/Metadata/ApiResource#provider) option. #[ApiResource(provider: BookProvider::class)] class Book { diff --git a/docs/guide/002-Hook-a-persistence-layer-with-a-Processor.php b/docs/guide/002-Hook-a-persistence-layer-with-a-Processor.php index 3d0664459ab..e7bbfee3630 100644 --- a/docs/guide/002-Hook-a-persistence-layer-with-a-Processor.php +++ b/docs/guide/002-Hook-a-persistence-layer-with-a-Processor.php @@ -3,6 +3,7 @@ // slug: hook-a-persistence-layer-with-a-processor // name: Hook a Persistence Layer with a Processor // position: 2 +// executable: true // --- // # Hook a Persistence Layer with a Processor diff --git a/docs/guide/003-Validate-incoming-data.php b/docs/guide/003-Validate-incoming-data.php index 8cfda3e1eda..17cc76ef0f7 100644 --- a/docs/guide/003-Validate-incoming-data.php +++ b/docs/guide/003-Validate-incoming-data.php @@ -3,6 +3,7 @@ // slug: use-validation-groups // name: Use Validation Groups // position: 3 +// executable: true // --- // # Validing incoming data diff --git a/docs/guide/004-Secure-a-Resource-Access.php b/docs/guide/004-Secure-a-Resource-Access.php new file mode 100644 index 00000000000..6b7dc2f540b --- /dev/null +++ b/docs/guide/004-Secure-a-Resource-Access.php @@ -0,0 +1,57 @@ +security = $security; + } + + protected function supports($attribute, $subject): bool + { + // It supports several attributes related to our Resource access control. + $supportsAttribute = in_array($attribute, ['BOOK_CREATE', 'BOOK_READ', 'BOOK_EDIT', 'BOOK_DELETE']); + $supportsSubject = $subject instanceof Book; + + return $supportsAttribute && $supportsSubject; + } + + /** + * @param string $attribute + * @param Book $subject + * @param TokenInterface $token + * @return bool + */ + protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool + { + /** ... check if the user is anonymous ... **/ + + switch ($attribute) { + case 'BOOK_CREATE': + if ( $this->security->isGranted(Role::ADMIN) ) { return true; } // only admins can create books + break; + case 'BOOK_READ': + /** ... other autorization rules ... **/ + } + + return false; + } + } +} + +namespace App\ApiResource { + use ApiPlatform\Metadata\ApiResource; + use ApiPlatform\Metadata\Delete; + use ApiPlatform\Metadata\Get; + use ApiPlatform\Metadata\GetCollection; + use ApiPlatform\Metadata\Post; + use ApiPlatform\Metadata\Put; + + #[ApiResource(security: "is_granted('ROLE_USER')")] + // We can then use the `is_granted` expression with our access control attributes: + #[Get(security: "is_granted('BOOK_READ', object)")] + #[Put(security: "is_granted('BOOK_EDIT', object)")] + #[Delete(security: "is_granted('BOOK_DELETE', object)")] + // On a collection, you need to [implement a Provider](provide-the-resource-state) to filter the collection manually. + #[GetCollection] + // `object` is empty uppon creation, we use `securityPostDenormalize` to get the denormalized object. + #[Post(securityPostDenormalize: "is_granted('BOOK_CREATE', object)")] + class Book + { + // ... + } +} diff --git a/docs/guide/015-extend-openapi-data.php b/docs/guide/015-extend-openapi-data.php new file mode 100644 index 00000000000..7dfde8abff0 --- /dev/null +++ b/docs/guide/015-extend-openapi-data.php @@ -0,0 +1,58 @@ +decorated = $decorated; + } + + public function __invoke(array $context = []): OpenApi + { + $openApi = $this->decorated->__invoke($context); + $pathItem = $openApi->getPaths()->getPath('/api/grumpy_pizzas/{id}'); + $operation = $pathItem->getGet(); + + $openApi->getPaths()->addPath('/api/grumpy_pizzas/{id}', $pathItem->withGet( + $operation->withParameters(array_merge( + $operation->getParameters(), + [new Model\Parameter('fields', 'query', 'Fields to remove of the output')] + )) + )); + + $openApi = $openApi->withInfo((new Model\Info('New Title', 'v2', 'Description of my custom API'))->withExtensionProperty('info-key', 'Info value')); + $openApi = $openApi->withExtensionProperty('key', 'Custom x-key value'); + $openApi = $openApi->withExtensionProperty('x-value', 'Custom x-value value'); + + return $openApi; + } + } +} + +namespace App\Configurator { + use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; + + function configure(ContainerConfigurator $configurator) { + $services = $configurator->services(); + $services->set(App\OpenApi\OpenApiFactory::class)->decorate('api_platform.openapi.factory'); + }; +} + +// namespace App\Tests { +// class ApiTestCase extends ApiTestCase { +// +// } +// } diff --git a/docs/guide/099-Validate-data-on-a-Delete-Operation.php b/docs/guide/099-Validate-data-on-a-Delete-Operation.php index 30dd24a3be7..14ab7ff2080 100644 --- a/docs/guide/099-Validate-data-on-a-Delete-Operation.php +++ b/docs/guide/099-Validate-data-on-a-Delete-Operation.php @@ -5,7 +5,6 @@ // position: 99 // --- -/ // Let's add a [custom Constraint](https://symfony.com/doc/current/validation/custom_constraint.html). namespace App\Validator { use Symfony\Component\Validator\Constraint; @@ -25,7 +24,7 @@ class AssertCanDeleteValidator extends ConstraintValidator { public function validate(mixed $value, Constraint $constraint) { - // TODO: Implement validate() method. + /* TODO: Implement validate() method. */ } } } @@ -81,10 +80,8 @@ public function process($data, Operation $operation, array $uriVariables = [], a } } - // TODO move this to reference somehow // This operation uses a Callable as group so that you can vary the Validation according to your dataset // new Get(validationContext: ['groups' =>]) // ## Sequential Validation Groups // If you need to specify the order in which your validation groups must be tested against, you can use a [group sequence](http://symfony.com/doc/current/validation/sequence_provider.html). - diff --git a/docs/explanation/doctrine.md b/docs/in-depth/doctrine.md similarity index 100% rename from docs/explanation/doctrine.md rename to docs/in-depth/doctrine.md diff --git a/docs/in-depth/events.md b/docs/in-depth/events.md new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/in-depth/metadata.md b/docs/in-depth/metadata.md new file mode 100644 index 00000000000..cb72be01264 --- /dev/null +++ b/docs/in-depth/metadata.md @@ -0,0 +1,3 @@ +# Metadata + +explain metadata system, metadata factories diff --git a/docs/in-depth/operations.md b/docs/in-depth/operations.md new file mode 100644 index 00000000000..dbd0077a88b --- /dev/null +++ b/docs/in-depth/operations.md @@ -0,0 +1,150 @@ +# Operations + +API Platform relies on the concept of operations. Operations can be applied to a resource exposed by the API. From +an implementation point of view, an operation is a link between a resource, a route and its related controller. + +

Operations screencast
Watch the Operations screencast

+ +API Platform automatically registers typical [CRUD](https://en.wikipedia.org/wiki/Create,_read,_update_and_delete) operations +and describes them in the exposed documentation (Hydra and Swagger). It also creates and registers routes corresponding +to these operations in the Symfony routing system (if it is available). + +The behavior of built-in operations can be seen in the [Declare a Resource](/guide/declare-a-resource) guide. + +The list of enabled operations can be configured on a per-resource basis. Creating custom operations on specific routes +is also possible. We see an Operation as a method, URI Template, type (dictionnary vs collection) tuple. +Indeed the `ApiPlatform\Metadata\GetCollection` implements the `ApiPlatform\Metadata\CollectionOperationInterface`, meaning +that it returns a collection. + +When the `ApiPlatform\Metadata\ApiResource` attribute is applied to a PHP class, the following built-in CRUD +operations are automatically enabled: + +Method | Class | Mandatory* | Description +---------|--------------------------------------|------------|------------------------------ +`GET` | `ApiPlatform\Metadata\GetCollection` | yes | Retrieve the (paginated) list of elements +`POST` | `ApiPlatform\Metadata\Post` | no | Create a new element +`GET` | `ApiPlatform\Metadata\Get` | yes | Retrieve an element +`PUT` | `ApiPlatform\Metadata\Put` | no | Replace an element +`PATCH` | `ApiPlatform\Metadata\Patch` | no | Apply a partial modification to an element +`DELETE` | `ApiPlatform\Metadata\Delete` | no | Delete an element + +*These operations are mandatory as they play a role in [IRI](./uri) generation. + + +If no operation is specified, all default CRUD operations are automatically registered. It is also possible - and recommended +for large projects - to define operations explicitly. + +Keep in mind that once you explicitly set up an operation, the automatically registered CRUD will no longer be. +If you declare even one operation manually, such as `#[ApiPlatform\Metadata\Get]`, you must declare the others manually as well if you need them. + +Operations can be configured using annotations, XML or YAML. In the following examples, we enable only the built-in operation +read operations for both a collection of books and a single book endpoint. + +[codeSelector] + +```php + + + + + + + + + + + +``` + +[/codeSelector] + + +If you do not want to allow access to the resource item (i.e. you don't want a `GET` item operation), instead of omitting it altogether, you should instead declare a `GET` item operation which returns HTTP 404 (Not Found), so that the resource item can still be identified by an IRI. For example: + +[codeSelector] + +```php + + + + + + + + + + + +``` + +[/codeSelector] diff --git a/docs/explanation/uri.md b/docs/in-depth/uri.md similarity index 100% rename from docs/explanation/uri.md rename to docs/in-depth/uri.md diff --git a/docs/in-depth/validation.md b/docs/in-depth/validation.md new file mode 100644 index 00000000000..0d273dc5b08 --- /dev/null +++ b/docs/in-depth/validation.md @@ -0,0 +1,118 @@ +# Validation + +API Platform takes care of validating the data sent to the API by the client (usually user data entered through forms). +By default, the framework relies on [the powerful Symfony Validator Component](http://symfony.com/doc/current/validation.html) +for this task, but you can replace it with your preferred validation library such as [the PHP filter extension](https://www.php.net/manual/en/intro.filter.php) if you want to. + +

Validation screencast
Watch the Validation screencast

+ +## Symfony + +In relation to Symfony, API Platform hooks a [validation listener](./symfony-event-listeners) executed during the `kernel.view` event. It checks if the validation is disabled on your Operation via [Operation::validate](/reference/Metadata/Operation#validate), then it calls the [Symfony validator](https://symfony.com/doc/current/components/validator.html) with the [Operation::validationContext](/reference/Metadata/Operation#validationContext). + +If one wants to [use validation on a Delete operation](/guide/validate-data-on-a-delete-operation) you'd need to implement this yourself. + +## Error Levels + +If you need to customize error levels we recommend to follow the [Symfony documentation](https://symfony.com/doc/current/validation/severity.html) and add payload fields. +You can then retrieve the payload field by setting the `serialize_payload_fields` to an empty `array` in the API Platform configuration: + +```yaml +# api/config/packages/api_platform.yaml + +api_platform: + validator: + serialize_payload_fields: ~ +``` + +Then, the serializer will return all payload values in the error response. + +If you want to serialize only some payload fields, define them in the config like this: + +```yaml +# api/config/packages/api_platform.yaml + +api_platform: + validator: + serialize_payload_fields: [ severity, anotherPayloadField ] +``` + +In this example, only `severity` and `anotherPayloadField` will be serialized. + + +## JSON Schema and OpenAPI integration + +### Specification property restrictions + +API Platform generates specification property restrictions based on Symfony’s built-in validator. + +For example, from [`Regex`](https://symfony.com/doc/4.4/reference/constraints/Regex.html) constraint API + Platform builds the JSON Schema [`pattern`](https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#schema-object) restriction. + +For building custom property schema based on custom validation constraints you can create a custom class +for generating property scheme restriction. + +To create property schema, you have to implement the [`PropertySchemaRestrictionMetadataInterface`](https://github.com/api-platform/core/blob/main/src/Symfony/Validator/Metadata/Property/Restriction/PropertySchemaRestrictionMetadataInterface.php). +This interface defines only 2 methods: + +* `create`: to create property schema +* `supports`: to check whether the property and constraint is supported + +Here is an implementation example: + +```php +namespace App\PropertySchemaRestriction; + +use ApiPlatform\Metadata\ApiProperty; +use Symfony\Component\Validator\Constraint; +use App\Validator\CustomConstraint; + +final class CustomPropertySchemaRestriction implements PropertySchemaRestrictionMetadataInterface +{ + public function supports(Constraint $constraint, ApiProperty $propertyMetadata): bool + { + return $constraint instanceof CustomConstraint; + } + + public function create(Constraint $constraint, ApiProperty $propertyMetadata): array + { + // your logic to create property schema restriction based on constraint + return $restriction; + } +} +``` + +### Open Vocabulary Generated from Validation Metadata + +API Platform automatically detects Symfony's built-in validators and generates schema.org IRI metadata accordingly. This allows for rich clients such as the Admin component to infer the field types for most basic use cases. + +The following validation constraints are covered: + +Constraints | Vocabulary | +--------------------------------------------------------------------------------------|-----------------------------------| +[`Url`](https://symfony.com/doc/current/reference/constraints/Url.html) | `https://schema.org/url` | +[`Email`](https://symfony.com/doc/current/reference/constraints/Email.html) | `https://schema.org/email` | +[`Uuid`](https://symfony.com/doc/current/reference/constraints/Uuid.html) | `https://schema.org/identifier` | +[`CardScheme`](https://symfony.com/doc/current/reference/constraints/CardScheme.html) | `https://schema.org/identifier` | +[`Bic`](https://symfony.com/doc/current/reference/constraints/Bic.html) | `https://schema.org/identifier` | +[`Iban`](https://symfony.com/doc/current/reference/constraints/Iban.html) | `https://schema.org/identifier` | +[`Date`](https://symfony.com/doc/current/reference/constraints/Date.html) | `https://schema.org/Date` | +[`DateTime`](https://symfony.com/doc/current/reference/constraints/DateTime.html) | `https://schema.org/DateTime` | +[`Time`](https://symfony.com/doc/current/reference/constraints/Time.html) | `https://schema.org/Time` | +[`Image`](https://symfony.com/doc/current/reference/constraints/Image.html) | `https://schema.org/image` | +[`File`](https://symfony.com/doc/current/reference/constraints/File.html) | `https://schema.org/MediaObject` | +[`Currency`](https://symfony.com/doc/current/reference/constraints/Currency.html) | `https://schema.org/priceCurrency` | +[`Isbn`](https://symfony.com/doc/current/reference/constraints/Isbn.html) | `https://schema.org/isbn` | +[`Issn`](https://symfony.com/doc/current/reference/constraints/Issn.html) | `https://schema.org/issn` | + +## Query parameter validation + + +By default query parameter validation is on, you can remove it using: + +```yaml +# api/config/packages/api_platform.yaml + +api_platform: + validator: + query_parameter_validation: false diff --git a/docs/src/Command/GenerateConfigurationReferenceCommand.php b/docs/src/Command/GenerateConfigurationReferenceCommand.php deleted file mode 100644 index e445631aef5..00000000000 --- a/docs/src/Command/GenerateConfigurationReferenceCommand.php +++ /dev/null @@ -1,68 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace PDG\Command; - -use ApiPlatform\Symfony\Bundle\DependencyInjection\Configuration; -use Symfony\Component\Config\Definition\Dumper\XmlReferenceDumper; -use Symfony\Component\Config\Definition\Dumper\YamlReferenceDumper; -use Symfony\Component\Console\Attribute\AsCommand; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; - -#[AsCommand(name: 'pdg:generate-configuration')] -class GenerateConfigurationReferenceCommand extends Command -{ - protected function configure(): void - { - $this->addArgument( - 'output', - InputArgument::OPTIONAL, - 'The path to the mdx file where the reference will be printed.Leave empty for screen printing' - ); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $style = new SymfonyStyle($input, $output); - $style->info('Generating configuration reference'); - - $outputFile = $input->getArgument('output'); - - $yaml = (new YamlReferenceDumper())->dump(new Configuration()); - $xml = (new XmlReferenceDumper())->dump(new Configuration()); - if (!$xml && !$yaml) { - $style->error('No configuration is available'); - - return Command::FAILURE; - } - - $content = '# Configuration Reference'.\PHP_EOL; - $content .= "Here's the complete configuration of the Symfony bundle including default values: ".\PHP_EOL; - $content .= '```yaml'.\PHP_EOL.$yaml.'```'.\PHP_EOL; - $content .= 'Or if you prefer XML configuration: '.\PHP_EOL; - $content .= '```xml'.\PHP_EOL.$xml.'```'.\PHP_EOL; - - if (!fwrite(fopen($outputFile, 'w'), $content)) { - $style->error('Error opening or writing '.$outputFile); - - return Command::FAILURE; - } - $style->success('Configuration reference successfully generated'); - - return Command::SUCCESS; - } -} diff --git a/docs/src/Command/ReferenceCommand.php b/docs/src/Command/ReferenceCommand.php index 370fd903791..7a8f1929fad 100644 --- a/docs/src/Command/ReferenceCommand.php +++ b/docs/src/Command/ReferenceCommand.php @@ -61,12 +61,13 @@ protected function configure(): void protected function execute(InputInterface $input, OutputInterface $output): int { $style = new SymfonyStyle($input, $output); + $stderr = $style->getErrorStyle(); $fileName = $input->getArgument('filename'); $file = Path::makeAbsolute($fileName, getcwd()); $relative = Path::makeRelative($file, $this->root); - $style->info(sprintf('Generating reference for %s', $relative)); + $stderr->info(sprintf('Generating reference for %s', $relative)); $namespace = 'ApiPlatform\\'.str_replace(['/', '.php'], ['\\', ''], $relative); $content = ''; @@ -74,7 +75,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int $outputFile = $input->getArgument('output'); if ($this->reflectionClass->implementsInterface(ConfigurationInterface::class)) { - return $this->generateConfigExample($style, $outputFile); + return $this->generateConfigExample($stderr, $outputFile); } $content = $this->outputFormatter->writePageTitle($this->reflectionClass, $content); @@ -88,17 +89,17 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (!$outputFile) { fwrite(\STDOUT, $content); - $style->success('Reference successfully printed on stdout for '.$relative); + $stderr->success('Reference successfully printed on stdout for '.$relative); return Command::SUCCESS; } if (!fwrite(fopen($outputFile, 'w'), $content)) { - $style->error('Error opening or writing '.$outputFile); + $stderr->error('Error opening or writing '.$outputFile); return Command::FAILURE; } - $style->success('Reference successfully generated for '.$relative); + $stderr->success('Reference successfully generated for '.$relative); return Command::SUCCESS; } diff --git a/docs/src/Command/ReferencesCommand.php b/docs/src/Command/ReferencesCommand.php index 175e11f5fad..6c6df96672a 100644 --- a/docs/src/Command/ReferencesCommand.php +++ b/docs/src/Command/ReferencesCommand.php @@ -13,6 +13,7 @@ namespace PDG\Command; +use ApiPlatform\Metadata\Get; use PDG\Services\Reference\PhpDocHelper; use PDG\Services\Reference\Reflection\ReflectionHelper; use Symfony\Component\Console\Attribute\AsCommand; @@ -44,6 +45,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int { $style = new SymfonyStyle($input, $output); + // TODO: move this to a Sf Configuration $patterns = $this->config['reference']['patterns']; $referencePath = $this->config['sidebar']['directories']['Reference'][0]; $tagsToIgnore = $patterns['class-tags-to-ignore']; @@ -53,25 +55,42 @@ protected function execute(InputInterface $input, OutputInterface $output): int $files = $this->findFilesByName($patterns['names'], $files, $filesToExclude); $files = $this->findFilesByDirectories($patterns['directories'], $files, $filesToExclude); + $namespaces = []; + foreach ($files as $file) { $relativeToSrc = Path::makeRelative($file->getPath(), $this->root); $relativeToDocs = Path::makeRelative($file->getRealPath(), getcwd()); - $namespace = 'ApiPlatform\\'.str_replace(['/', '.php'], ['\\', ''], $relativeToSrc).'\\'.$file->getBasename('.php'); + $namespace = 'ApiPlatform\\'.str_replace(['/', '.php'], ['\\', ''], $relativeToSrc); + $className = sprintf('%s\\%s', $namespace, $file->getBasename('.php')); + $refl = new \ReflectionClass($className); + + if (!($namespaces[$namespace] ?? false)) { + $namespaces[$namespace] = []; + } + + $namespaces[$namespace][] = [ + 'className' => $className, + 'shortName' => $file->getBasename('.php'), + 'type' => $this->getClassType($refl), + 'link' => '/reference/' . ($relativeToSrc . $file->getBaseName('.php')) + ]; + foreach ($tagsToIgnore as $tagToIgnore) { - if ($this->phpDocHelper->classDocContainsTag(new \ReflectionClass($namespace), $tagToIgnore)) { + if ($this->phpDocHelper->classDocContainsTag($refl, $tagToIgnore)) { continue 2; } } - if ($this->reflectionHelper->containsOnlyPrivateMethods(new \ReflectionClass($namespace))) { + + if ($this->reflectionHelper->containsOnlyPrivateMethods($refl)) { continue; } if (!@mkdir($concurrentDirectory = $referencePath.'/'.$relativeToSrc, 0777, true) && !is_dir($concurrentDirectory)) { $style->error(sprintf('Directory "%s" was not created', $concurrentDirectory)); - return Command::FAILURE; } + $generateRefCommand = $this->getApplication()?->find('pdg:reference'); $arguments = [ @@ -83,11 +102,23 @@ protected function execute(InputInterface $input, OutputInterface $output): int if (Command::FAILURE === $generateRefCommand->run($commandInput, $output)) { $style->error(sprintf('Failed generating reference for %s', $file->getBaseNme())); - return Command::FAILURE; } } + // Creating an index like https://angular.io/api + $content = ''; + foreach ($namespaces as $namespace => $classes) { + $content .= '## ' . $namespace . PHP_EOL; + $content .= '' . PHP_EOL; + } + + fwrite(\STDOUT, $content); + return Command::SUCCESS; } @@ -110,4 +141,20 @@ private function findFilesByName(array $names, array $files, array $filesToExclu return $files; } + + private function getClassType(\ReflectionClass $refl): string { + if ($refl->isInterface()) { + return 'I'; + } + + if (\count($refl->getAttributes('Attribute'))) { + return 'A'; + } + + if ($refl->isTrait()) { + return 'T'; + } + + return 'C'; + } } diff --git a/docs/src/Services/Reference/PhpDocHelper.php b/docs/src/Services/Reference/PhpDocHelper.php index 04d6d3e1613..1908d6d5da5 100644 --- a/docs/src/Services/Reference/PhpDocHelper.php +++ b/docs/src/Services/Reference/PhpDocHelper.php @@ -65,7 +65,8 @@ public function handleClassDoc(\ReflectionClass $class, string $content): string /** @var PhpDocTextNode $t */ foreach ($text as $t) { - // todo {@see ... } breaks generation, but we can probably reference it better + // @TODO: this should be handled by the Javascript using `md` files as `mdx` we should not need this here + // indeed {@see} breaks mdx as it thinks that its a React component if (str_contains($t->text, '@see')) { $t = str_replace(['{@see', '}'], ['see', ''], $t->text); } diff --git a/docs/src/Services/Reference/Reflection/ReflectionHelper.php b/docs/src/Services/Reference/Reflection/ReflectionHelper.php index f8312a73cd0..38b6c196502 100644 --- a/docs/src/Services/Reference/Reflection/ReflectionHelper.php +++ b/docs/src/Services/Reference/Reflection/ReflectionHelper.php @@ -118,7 +118,7 @@ public function handleProperties(\ReflectionClass $reflectionClass, string $cont $content .= "### {$modifier} {$type} {$this->outputFormatter->addCssClasses('$'.$property->getName(), ['token', 'keyword'])}"; $content .= $defaultValue.\PHP_EOL; if ($additionalTypeInfo) { - $content .= '> Type from PHPDoc: '.$additionalTypeInfo.\PHP_EOL.\PHP_EOL; + $content .= '> '.$additionalTypeInfo.\PHP_EOL.\PHP_EOL; } if (!empty($accessors)) { $content .= '**Accessors**: '.implode(', ', $accessors).\PHP_EOL; diff --git a/docs/src/Services/Reference/Reflection/ReflectionMethodHelper.php b/docs/src/Services/Reference/Reflection/ReflectionMethodHelper.php index 1a04eaf4c1a..4858551099c 100644 --- a/docs/src/Services/Reference/Reflection/ReflectionMethodHelper.php +++ b/docs/src/Services/Reference/Reflection/ReflectionMethodHelper.php @@ -133,14 +133,15 @@ private function getParameterDefaultValueString(\ReflectionParameter $parameter) $traverser->traverse($stmts); $defaultValue = $visitor->defaultValue; + $prefix = ' '; return match (true) { null === $defaultValue => '', - $defaultValue instanceof Node\Scalar => '= '.$defaultValue->getAttribute('rawValue'), - $defaultValue instanceof Node\Expr\ConstFetch => '= '.$defaultValue->name->parts[0], - $defaultValue instanceof Node\Expr\New_ => sprintf('= new %s()', $defaultValue->class->parts[0]), - $defaultValue instanceof Node\Expr\Array_ => '= '.$this->outputFormatter->arrayNodeToString($defaultValue), - $defaultValue instanceof Node\Expr\ClassConstFetch => '= '.$defaultValue->class->parts[0].'::'.$defaultValue->name->name + $defaultValue instanceof Node\Scalar => $prefix.$defaultValue->getAttribute('rawValue'), + $defaultValue instanceof Node\Expr\ConstFetch => $prefix.$defaultValue->name->parts[0], + $defaultValue instanceof Node\Expr\New_ => sprintf('%s new %s()', $prefix, $defaultValue->class->parts[0]), + $defaultValue instanceof Node\Expr\Array_ => $prefix.$this->outputFormatter->arrayNodeToString($defaultValue), + $defaultValue instanceof Node\Expr\ClassConstFetch => $prefix.$defaultValue->class->parts[0].'::'.$defaultValue->name->name }; } } diff --git a/docs/src/Services/Reference/Reflection/ReflectionPropertyHelper.php b/docs/src/Services/Reference/Reflection/ReflectionPropertyHelper.php index f899f69876f..48afe24a5f1 100644 --- a/docs/src/Services/Reference/Reflection/ReflectionPropertyHelper.php +++ b/docs/src/Services/Reference/Reflection/ReflectionPropertyHelper.php @@ -70,14 +70,15 @@ public function getPromotedPropertyDefaultValueString(\ReflectionProperty $refle $traverser->traverse($stmts); $defaultValue = $visitor->defaultValue; + $prefix = ' = '; return match (true) { null === $defaultValue => '', - $defaultValue instanceof Node\Scalar => '= '.$defaultValue->getAttribute('rawValue'), - $defaultValue instanceof Node\Expr\ConstFetch => '= '.$defaultValue->name->parts[0], - $defaultValue instanceof Node\Expr\New_ => sprintf('= new %s()', $defaultValue->class->parts[0]), - $defaultValue instanceof Node\Expr\Array_ => '= '.$this->outputFormatter->arrayNodeToString($defaultValue), - $defaultValue instanceof Node\Expr\ClassConstFetch => '= '.$defaultValue->class->parts[0].'::'.$defaultValue->name->name + $defaultValue instanceof Node\Scalar => $prefix.$defaultValue->getAttribute('rawValue'), + $defaultValue instanceof Node\Expr\ConstFetch => $prefix.$defaultValue->name->parts[0], + $defaultValue instanceof Node\Expr\New_ => sprintf('%s new %s()', $prefix, $defaultValue->class->parts[0]), + $defaultValue instanceof Node\Expr\Array_ => $prefix.$this->outputFormatter->arrayNodeToString($defaultValue), + $defaultValue instanceof Node\Expr\ClassConstFetch => $prefix.$defaultValue->class->parts[0].'::'.$defaultValue->name->name }; } @@ -137,14 +138,15 @@ public function getPropertyDefaultValueString(\ReflectionProperty $property): st $traverser->traverse($stmts); $defaultValue = $visitor->defaultValue; + $prefix = ' = '; return match (true) { null === $defaultValue => '', - $defaultValue instanceof Node\Scalar => '= '.$defaultValue->getAttribute('rawValue'), - $defaultValue instanceof Node\Expr\ConstFetch => '= '.$defaultValue->name->parts[0], - $defaultValue instanceof Node\Expr\New_ => sprintf('= new %s()', $defaultValue->class->parts[0]), - $defaultValue instanceof Node\Expr\Array_ => '= '.$this->outputFormatter->arrayNodeToString($defaultValue), - $defaultValue instanceof Node\Expr\ClassConstFetch => '= '.$defaultValue->class->parts[0].'::'.$defaultValue->name->name + $defaultValue instanceof Node\Scalar => $prefix.$defaultValue->getAttribute('rawValue'), + $defaultValue instanceof Node\Expr\ConstFetch => $prefix.$defaultValue->name->parts[0], + $defaultValue instanceof Node\Expr\New_ => sprintf('%s new %s()', $prefix, $defaultValue->class->parts[0]), + $defaultValue instanceof Node\Expr\Array_ => $prefix.$this->outputFormatter->arrayNodeToString($defaultValue), + $defaultValue instanceof Node\Expr\ClassConstFetch => $prefix.$defaultValue->class->parts[0].'::'.$defaultValue->name->name }; } }