Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions src/Maker/MakeDataPersister.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
<?php

/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\MakerBundle\Maker;

use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceMetadataFactoryInterface;
use ApiPlatform\Core\Metadata\Resource\Factory\ResourceNameCollectionFactoryInterface;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Question\Question;

/**
* @author Imad ZAIRIG <imadzairig@gmail.com>
*
* @internal
*/
final class MakeDataPersister extends AbstractMaker
{
/** @var ResourceNameCollectionFactoryInterface */
protected $ressourceNameCollectionFactory;
/** @var ResourceMetadataFactoryInterface */
protected $ressourceMetaDataFactory;
protected $fileManager;
protected $doctrineHelper;
protected $resourcesClassNames = [];

public function __construct(
FileManager $fileManager,
DoctrineHelper $doctrineHelper,
$ressourceNameCollectionFactory = null,
$ressourceMetaDataFactory = null
) {
$this->fileManager = $fileManager;
$this->doctrineHelper = $doctrineHelper;
$this->ressourceNameCollection = $ressourceNameCollectionFactory;
$this->ressourceMetaDataFactory = $ressourceMetaDataFactory;
}

public static function getCommandName(): string
{
return 'make:api:data-persister';
}

public function configureCommand(Command $command, InputConfiguration $inputConfig)
{
$command
->setDescription('Creates a API Platform Data Persister')
->addArgument('name', InputArgument::OPTIONAL, 'The name of the Data Persister class (e.g. <fg=yellow>CustomDataPersister</>)')
->addArgument('resource', InputArgument::OPTIONAL, 'The name of the resource class')
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeDataPersister.txt'));
$inputConfig->setArgumentAsNonInteractive('resource');
}

public function interact(InputInterface $input, ConsoleStyle $io, Command $command)
{
if (null === $input->getArgument('resource')) {
$argument = $command->getDefinition()->getArgument('resource');
$question = $this->createResourceClassQuestion($argument->getDescription());
$value = $io->askQuestion($question);
$input->setArgument('resource', $value);
$doctrineOption = new InputOption('is_doctrine_persister', 'a', InputOption::VALUE_NONE, 'Would you like your persister to call the core Doctrine persister?');
$command->getDefinition()->addOption($doctrineOption);
$this->resourcesClassNames = array_flip($this->getResources());

if (\in_array($value, $this->getResources()) && $this->doctrineHelper->isClassAMappedEntity($this->resourcesClassNames[$value])) {
$description = $command->getDefinition()->getOption('is_doctrine_persister')->getDescription();
$question = new ConfirmationQuestion($description, false);
$value = $io->askQuestion($question);

$input->setOption('is_doctrine_persister', $value);
}
}
}

public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
{
$templateVariables = [];
$resourceShortName = $input->getArgument('resource');
$this->resourcesClassNames = array_flip($this->getResources());

if ($resourceShortName && \in_array($resourceShortName, $this->resourcesClassNames)) {
$resourceClasseName = $this->resourcesClassNames[$resourceShortName];
$templateVariables['resource_short_name'] = $resourceShortName;
$templateVariables['resource_class_name'] = $resourceClasseName;
}

$dataPersisterClassNameDetails = $generator->createClassNameDetails(
$input->getArgument('name'),
'DataPersister\\',
'DataPersister'
);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might make sense to allow the user to interactively specify what class the data persister will persist for - e.g. App\Entity\CheeseListing. It would be even cooler if this read the API Platform metadata to list all the API Platform resource classes :). This would help us generate a better supports() method. Or, they could leave this blank and we would generate the supports() method like they have now.

And, to go further, if the resource class they choose is an entity, we could ask them something like this:

Would you like your persister to call the core Doctrine persister? This would allow you to
add custom logic without needing to worry about the save logic.

If they said yes, we would inject the core Doctrine ORM persister like done here - https://symfonycasts.com/screencast/api-platform-extending/decoration-deep-dive

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, great ideas, I'm gonna Try to add this, and make Seperate PRs for the other Makers :)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weaverryan the Maker is now working with the update of services and with the different cases.
I would love to see your feedback if there are somethings to refactor in the code.


if ($input->getOption('is_doctrine_persister')) {
$templateVariables['is_doctrine_persister'] = true;
if (!$this->fileManager->fileExists($path = 'config/services.yaml')) {
throw new RuntimeCommandException('The file "config/packages/security.yaml" does not exist. This command requires that file to exist so that it can be updated.');
}
$manipulator = new YamlSourceManipulator($this->fileManager->getFileContents($path));

$servicesData = $manipulator->getData();
$servicesData['services'] = [$dataPersisterClassNameDetails->getFullName() => ['decorates' => 'api_platform.doctrine.orm.data_persister']] + $servicesData['services'];
$manipulator->setData($servicesData);
$this->fileManager->dumpFile($path, $manipulator->getContents());
}

$generator->generateClass(
$dataPersisterClassNameDetails->getFullName(),
'api/DataPersister.tpl.php',
$templateVariables
);

$generator->writeChanges();

$this->writeSuccessMessage($io);

$io->text([
'Next:',
sprintf('- Open the <info>%s</info> class and add the code you need', $dataPersisterClassNameDetails->getFullName()),
'Find the documentation at <fg=yellow>https://api-platform.com/docs/core/data-persisters/#creating-a-custom-data-persister</>',
]);
}

public function configureDependencies(DependencyBuilder $dependencies)
{
$dependencies->addClassDependency(
ContextAwareDataPersisterInterface::class,
'api'
);
}

private function createResourceClassQuestion(string $questionText): Question
{
$question = new Question($questionText);
$question->setAutocompleterValues(array_values($this->getResources()));

return $question;
}

private function getResources(): array
{
$collection = $this->ressourceNameCollection->create();
$resources = [];

foreach ($collection as $className) {
$resources[$className] = $this->ressourceMetaDataFactory->create($className)->getShortName();
}

return $resources;
}
}
8 changes: 8 additions & 0 deletions src/Resources/config/makers.xml
Original file line number Diff line number Diff line change
Expand Up @@ -123,5 +123,13 @@
<argument>%kernel.project_dir%</argument>
<tag name="maker.command" />
</service>

<service id="maker.maker.make_data_persister" class="Symfony\Bundle\MakerBundle\Maker\MakeDataPersister">
<argument type="service" id="maker.file_manager" />
<argument type="service" id="maker.doctrine_helper"/>
<argument type="service" id="api_platform.metadata.resource.name_collection_factory" on-invalid="ignore"/>
<argument type="service" id="api_platform.metadata.resource.metadata_factory" on-invalid="ignore"/>
<tag name="maker.command" />
</service>
</services>
</container>
5 changes: 5 additions & 0 deletions src/Resources/help/MakeDataPersister.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
The <info>%command.name%</info> command generates a new Data Persister class.

<info>php %command.full_name% CustomDataPersister</info>

If the argument is missing, the command will ask for the message class interactively.
45 changes: 45 additions & 0 deletions src/Resources/skeleton/api/DataPersister.tpl.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?= "<?php\n" ?>

namespace <?= $namespace; ?>;

use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
<?= isset($resource_class_name) ? "use $resource_class_name;\n" : '' ?>

final class <?= $class_name; ?> implements ContextAwareDataPersisterInterface
{
<?php if (isset($is_doctrine_persister) && isset($resource_short_name)): ?>
private $decorated;

public function __construct(ContextAwareDataPersisterInterface $decorated)
{
$this->decorated = $decorated;
}

<?php endif ?>
public function supports($data, array $context = []): bool
{
<?php if (isset($is_doctrine_persister) && isset($resource_short_name)): ?>
return $this->decorated->supports($data, $context);
<?php elseif (isset($resource_short_name)):?>
return $data instanceof <?= $resource_short_name; ?>;
<?php else: ?>
return true;
<?php endif ?>
}

public function persist($data, array $context = [])
{
<?php if (isset($is_doctrine_persister) && isset($resource_short_name)): ?>
$data = $this->decorated->persist($data, $context);

<?php endif ?>
return $data;
}

public function remove($data, array $context = [])
{
<?php if (isset($is_doctrine_persister) && isset($resource_short_name)): ?>
return $this->decorated->persist($data, $context);
<?php endif ?>
}
}
67 changes: 67 additions & 0 deletions tests/Maker/MakeDataPersisterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<?php

/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Bundle\MakerBundle\Tests\Maker;

use Symfony\Bundle\MakerBundle\Maker\MakeDataPersister;
use Symfony\Bundle\MakerBundle\Test\MakerTestCase;
use Symfony\Bundle\MakerBundle\Test\MakerTestDetails;

class MakeDataPersisterTest extends MakerTestCase
{
public function getTestDetails()
{
yield 'api_data_persister' => [MakerTestDetails::createTest(
$this->getMakerInstance(MakeDataPersister::class),
[
// data persister name
'CustomDataPersister',
' ',
])->assert(function (string $output, string $directory) {
$this->assertFileExists($directory.'/src/DataPersister/CustomDataPersister.php');
}),
];
yield 'entity_with_doctrine_persister' => [MakerTestDetails::createTest(
$this->getMakerInstance(MakeDataPersister::class),
[
'ArticleDataPersister',
'Article',
'yes',
])
->setFixtureFilesPath(__DIR__.'/../fixtures/MakeDataPersister')
->assert(function (string $output, string $directory) {
$this->assertFileExists($directory.'/src/DataPersister/ArticleDataPersister.php');
}),
];
yield 'entity_without_doctrine_persister' => [MakerTestDetails::createTest(
$this->getMakerInstance(MakeDataPersister::class),
[
'ArticleBlogDataPersister',
'Article',
])
->setFixtureFilesPath(__DIR__.'/../fixtures/MakeDataPersister')
->assert(function (string $output, string $directory) {
$this->assertFileExists($directory.'/src/DataPersister/ArticleBlogDataPersister.php');
}),
];
yield 'model_class_persister' => [MakerTestDetails::createTest(
$this->getMakerInstance(MakeDataPersister::class),
[
'BookDataPersister',
'Book',
])
->setFixtureFilesPath(__DIR__.'/../fixtures/MakeDataPersister')
->assert(function (string $output, string $directory) {
$this->assertFileExists($directory.'/src/DataPersister/BookDataPersister.php');
}),
];
}
}
32 changes: 32 additions & 0 deletions tests/fixtures/MakeDataPersister/Model/Book.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

namespace App\Model;

use ApiPlatform\Core\Annotation\ApiResource;

/**
* @ApiResource()
*/
class Book
{
private $id;

private $title;

public function getId(): ?int
{
return $this->id;
}

public function getTitle(): ?string
{
return $this->title;
}

public function setTitle(string $title): self
{
$this->title = $title;

return $this;
}
}
42 changes: 42 additions & 0 deletions tests/fixtures/MakeDataPersister/src/Entity/Article.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

namespace App\Entity;

use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\ORM\Mapping as ORM;

/**
* @ApiResource()
* @ORM\Entity()
*/
class Article
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;

/**
* @ORM\Column(type="string", length=255)
*/
private $title;

public function getId(): ?int
{
return $this->id;
}

public function getTitle(): ?string
{
return $this->title;
}

public function setTitle(string $title): self
{
$this->title = $title;

return $this;
}
}