EC-CUBE4のCustomizeディレクトリにEntityとRepositoryを自動生成するコマンドを用意する方法です。
今回はSymfonyのMakerBundleを拡張してEC-CUBE4のCustomizeディレクトリに自動生成するようにします。
まずはconfig/eccube/pakagesディレクトリ内に下記のmaker.yamlファイルを設置してください。
maker:
root_namespace: 'Customize'
次に下記のapp/Customize/Maker/MakerEntity.phpを設置してください。
<?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 Customize\Maker;
use ApiPlatform\Core\Annotation\ApiResource;
use Doctrine\DBAL\Types\Type;
use Doctrine\ORM\Mapping\Driver\AnnotationDriver;
use Symfony\Bundle\MakerBundle\ConsoleStyle;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\Doctrine\DoctrineHelper;
use Symfony\Bundle\MakerBundle\Doctrine\EntityClassGenerator;
use Symfony\Bundle\MakerBundle\Doctrine\ORMDependencyBuilder;
use Symfony\Bundle\MakerBundle\Exception\RuntimeCommandException;
use Symfony\Bundle\MakerBundle\Generator;
use Symfony\Bundle\MakerBundle\InputAwareMakerInterface;
use Symfony\Bundle\MakerBundle\InputConfiguration;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Bundle\MakerBundle\Doctrine\EntityRegenerator;
use Symfony\Bundle\MakerBundle\FileManager;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Bundle\MakerBundle\Doctrine\EntityRelation;
use Symfony\Bundle\MakerBundle\Validator;
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;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Bundle\MakerBundle\Maker\AbstractMaker;
/**
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
* @author Ryan Weaver <weaverryan@gmail.com>
* @author Kévin Dunglas <dunglas@gmail.com>
*/
final class MakeEntity extends AbstractMaker implements InputAwareMakerInterface
{
private $fileManager;
private $doctrineHelper;
private $generator;
public function __construct(FileManager $fileManager, DoctrineHelper $doctrineHelper, Generator $generator = null)
{
$this->fileManager = $fileManager;
$this->doctrineHelper = $doctrineHelper;
// $projectDirectory is unused, argument kept for BC
if (null === $generator) {
@trigger_error(sprintf('Passing a "%s" instance as 4th argument is mandatory since version 1.5.', Generator::class), E_USER_DEPRECATED);
$this->generator = new Generator($fileManager, 'App\\');
} else {
$this->generator = $generator;
}
}
public static function getCommandName(): string
{
return 'make:customize:entity';
}
public function configureCommand(Command $command, InputConfiguration $inputConf)
{
$command
->setDescription('Creates or updates a Doctrine entity class, and optionally an API Platform resource')
->addArgument('name', InputArgument::OPTIONAL, sprintf('Class name of the entity to create or update (e.g. <fg=yellow>%s</>)', Str::asClassName(Str::getRandomTerm())))
->addOption('api-resource', 'a', InputOption::VALUE_NONE, 'Mark this class as an API Platform resource (expose a CRUD API for it)')
->addOption('regenerate', null, InputOption::VALUE_NONE, 'Instead of adding new fields, simply generate the methods (e.g. getter/setter) for existing fields')
->addOption('overwrite', null, InputOption::VALUE_NONE, 'Overwrite any existing getter/setter methods')
->setHelp(file_get_contents(__DIR__.'/../Resources/help/MakeEntity.txt'))
;
$inputConf->setArgumentAsNonInteractive('name');
}
public function interact(InputInterface $input, ConsoleStyle $io, Command $command)
{
if ($input->getArgument('name')) {
return;
}
if ($input->getOption('regenerate')) {
$io->block([
'This command will generate any missing methods (e.g. getters & setters) for a class or all classes in a namespace.',
'To overwrite any existing methods, re-run this command with the --overwrite flag',
], null, 'fg=yellow');
$classOrNamespace = $io->ask('Enter a class or namespace to regenerate', $this->getEntityNamespace(), [Validator::class, 'notBlank']);
$input->setArgument('name', $classOrNamespace);
return;
}
$entityFinder = $this->fileManager->createFinder('app/Customize/Entity/')
// remove if/when we allow entities in subdirectories
->depth('<1')
->name('*.php');
$classes = [];
/** @var SplFileInfo $item */
foreach ($entityFinder as $item) {
if (!$item->getRelativePathname()) {
continue;
}
$classes[] = str_replace(['.php', '/'], ['', '\\'], $item->getRelativePathname());
}
$argument = $command->getDefinition()->getArgument('name');
$question = $this->createEntityClassQuestion($argument->getDescription());
$value = $io->askQuestion($question);
$input->setArgument('name', $value);
if (
!$input->getOption('api-resource') &&
class_exists(ApiResource::class) &&
!class_exists($this->generator->createClassNameDetails($value, 'Entity\\')->getFullName())
) {
$description = $command->getDefinition()->getOption('api-resource')->getDescription();
$question = new ConfirmationQuestion($description, false);
$value = $io->askQuestion($question);
$input->setOption('api-resource', $value);
}
}
public function generate(InputInterface $input, ConsoleStyle $io, Generator $generator)
{
$overwrite = $input->getOption('overwrite');
// the regenerate option has entirely custom behavior
if ($input->getOption('regenerate')) {
$this->regenerateEntities($input->getArgument('name'), $overwrite, $generator);
$this->writeSuccessMessage($io);
return;
}
$entityClassDetails = $generator->createClassNameDetails(
$input->getArgument('name'),
'Entity\\'
);
$classExists = class_exists($entityClassDetails->getFullName());
if (!$classExists) {
$entityClassGenerator = new EntityClassGenerator($generator);
$entityPath = $entityClassGenerator->generateEntityClass(
$entityClassDetails,
$input->getOption('api-resource')
);
$generator->writeChanges();
}
if (!$this->doesEntityUseAnnotationMapping($entityClassDetails->getFullName())) {
throw new RuntimeCommandException(sprintf('Only annotation mapping is supported by make:customize:entity, but the <info>%s</info> class uses a different format. If you would like this command to generate the properties & getter/setter methods, add your mapping configuration, and then re-run this command with the <info>--regenerate</info> flag.', $entityClassDetails->getFullName()));
}
if ($classExists) {
$entityPath = $this->getPathOfClass($entityClassDetails->getFullName());
$io->text([
'Your entity already exists! So let\'s add some new fields!',
]);
} else {
$io->text([
'',
'Entity generated! Now let\'s add some fields!',
'You can always add more fields later manually or by re-running this command.',
]);
}
$currentFields = $this->getPropertyNames($entityClassDetails->getFullName());
$manipulator = $this->createClassManipulator($entityPath, $io, $overwrite);
$isFirstField = true;
while (true) {
$newField = $this->askForNextField($io, $currentFields, $entityClassDetails->getFullName(), $isFirstField);
$isFirstField = false;
if (null === $newField) {
break;
}
$fileManagerOperations = [];
$fileManagerOperations[$entityPath] = $manipulator;
if (\is_array($newField)) {
$annotationOptions = $newField;
unset($annotationOptions['fieldName']);
$manipulator->addEntityField($newField['fieldName'], $annotationOptions);
$currentFields[] = $newField['fieldName'];
} elseif ($newField instanceof EntityRelation) {
// both overridden below for OneToMany
$newFieldName = $newField->getOwningProperty();
$otherManipulatorFilename = $this->getPathOfClass($newField->getInverseClass());
$otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite);
switch ($newField->getType()) {
case EntityRelation::MANY_TO_ONE:
if ($newField->getOwningClass() === $entityClassDetails->getFullName()) {
// THIS class will receive the ManyToOne
$manipulator->addManyToOneRelation($newField->getOwningRelation());
if ($newField->getMapInverseRelation()) {
$otherManipulator->addOneToManyRelation($newField->getInverseRelation());
}
} else {
// the new field being added to THIS entity is the inverse
$newFieldName = $newField->getInverseProperty();
$otherManipulatorFilename = $this->getPathOfClass($newField->getOwningClass());
$otherManipulator = $this->createClassManipulator($otherManipulatorFilename, $io, $overwrite);
// The *other* class will receive the ManyToOne
$otherManipulator->addManyToOneRelation($newField->getOwningRelation());
if (!$newField->getMapInverseRelation()) {
throw new \Exception('Somehow a OneToMany relationship is being created, but the inverse side will not be mapped?');
}
$manipulator->addOneToManyRelation($newField->getInverseRelation());
}
break;
case EntityRelation::MANY_TO_MANY:
$manipulator->addManyToManyRelation($newField->getOwningRelation());
if ($newField->getMapInverseRelation()) {
$otherManipulator->addManyToManyRelation($newField->getInverseRelation());
}
break;
case EntityRelation::ONE_TO_ONE:
$manipulator->addOneToOneRelation($newField->getOwningRelation());
if ($newField->getMapInverseRelation()) {
$otherManipulator->addOneToOneRelation($newField->getInverseRelation());
}
break;
default:
throw new \Exception('Invalid relation type');
}
// save the inverse side if it's being mapped
if ($newField->getMapInverseRelation()) {
$fileManagerOperations[$otherManipulatorFilename] = $otherManipulator;
}
$currentFields[] = $newFieldName;
} else {
throw new \Exception('Invalid value');
}
foreach ($fileManagerOperations as $path => $manipulatorOrMessage) {
if (\is_string($manipulatorOrMessage)) {
$io->comment($manipulatorOrMessage);
} else {
$this->fileManager->dumpFile($path, $manipulatorOrMessage->getSourceCode());
}
}
}
$this->writeSuccessMessage($io);
$io->text([
'Next: When you\'re ready, create a migration with <comment>make:migration</comment>',
'',
]);
}
public function configureDependencies(DependencyBuilder $dependencies, InputInterface $input = null)
{
$dependencies->requirePHP71();
if (null !== $input && $input->getOption('api-resource')) {
$dependencies->addClassDependency(
ApiResource::class,
'api'
);
}
ORMDependencyBuilder::buildDependencies($dependencies);
}
private function askForNextField(ConsoleStyle $io, array $fields, string $entityClass, bool $isFirstField)
{
$io->writeln('');
if ($isFirstField) {
$questionText = 'New property name (press <return> to stop adding fields)';
} else {
$questionText = 'Add another property? Enter the property name (or press <return> to stop adding fields)';
}
$fieldName = $io->ask($questionText, null, function ($name) use ($fields) {
// allow it to be empty
if (!$name) {
return $name;
}
if (\in_array($name, $fields)) {
throw new \InvalidArgumentException(sprintf('The "%s" property already exists.', $name));
}
return Validator::validateDoctrineFieldName($name, $this->doctrineHelper->getRegistry());
});
if (!$fieldName) {
return null;
}
$defaultType = 'string';
// try to guess the type by the field name prefix/suffix
// convert to snake case for simplicity
$snakeCasedField = Str::asSnakeCase($fieldName);
if ('_at' === $suffix = substr($snakeCasedField, -3)) {
$defaultType = 'datetime';
} elseif ('_id' === $suffix) {
$defaultType = 'integer';
} elseif (0 === strpos($snakeCasedField, 'is_')) {
$defaultType = 'boolean';
} elseif (0 === strpos($snakeCasedField, 'has_')) {
$defaultType = 'boolean';
}
$type = null;
$allValidTypes = array_merge(
array_keys(Type::getTypesMap()),
EntityRelation::getValidRelationTypes(),
['relation']
);
while (null === $type) {
$question = new Question('Field type (enter <comment>?</comment> to see all types)', $defaultType);
$question->setAutocompleterValues($allValidTypes);
$type = $io->askQuestion($question);
if ('?' === $type) {
$this->printAvailableTypes($io);
$io->writeln('');
$type = null;
} elseif (!\in_array($type, $allValidTypes)) {
$this->printAvailableTypes($io);
$io->error(sprintf('Invalid type "%s".', $type));
$io->writeln('');
$type = null;
}
}
if ('relation' === $type || \in_array($type, EntityRelation::getValidRelationTypes())) {
return $this->askRelationDetails($io, $entityClass, $type, $fieldName);
}
// this is a normal field
$data = ['fieldName' => $fieldName, 'type' => $type];
if ('string' == $type) {
// default to 255, avoid the question
$data['length'] = $io->ask('Field length', 255, [Validator::class, 'validateLength']);
} elseif ('decimal' === $type) {
// 10 is the default value given in \Doctrine\DBAL\Schema\Column::$_precision
$data['precision'] = $io->ask('Precision (total number of digits stored: 100.00 would be 5)', 10, [Validator::class, 'validatePrecision']);
// 0 is the default value given in \Doctrine\DBAL\Schema\Column::$_scale
$data['scale'] = $io->ask('Scale (number of decimals to store: 100.00 would be 2)', 0, [Validator::class, 'validateScale']);
}
if ($io->confirm('Can this field be null in the database (nullable)', false)) {
$data['nullable'] = true;
}
return $data;
}
private function printAvailableTypes(ConsoleStyle $io)
{
$allTypes = Type::getTypesMap();
$typesTable = [
'main' => [
'string' => [],
'text' => [],
'boolean' => [],
'integer' => ['smallint', 'bigint'],
'float' => [],
],
'relation' => [
'relation' => 'a wizard will help you build the relation',
EntityRelation::MANY_TO_ONE => [],
EntityRelation::ONE_TO_MANY => [],
EntityRelation::MANY_TO_MANY => [],
EntityRelation::ONE_TO_ONE => [],
],
'array_object' => [
'array' => ['simple_array'],
'json' => [],
'object' => [],
'binary' => [],
'blob' => [],
],
'date_time' => [
'datetime' => ['datetime_immutable'],
'datetimetz' => ['datetimetz_immutable'],
'date' => ['date_immutable'],
'time' => ['time_immutable'],
'dateinterval' => [],
],
];
$printSection = function (array $sectionTypes) use ($io, &$allTypes) {
foreach ($sectionTypes as $mainType => $subTypes) {
unset($allTypes[$mainType]);
$line = sprintf(' * <comment>%s</comment>', $mainType);
if (\is_string($subTypes) && $subTypes) {
$line .= sprintf(' (%s)', $subTypes);
} elseif (\is_array($subTypes) && !empty($subTypes)) {
$line .= sprintf(' (or %s)', implode(', ', array_map(function ($subType) {
return sprintf('<comment>%s</comment>', $subType);
}, $subTypes)));
foreach ($subTypes as $subType) {
unset($allTypes[$subType]);
}
}
$io->writeln($line);
}
$io->writeln('');
};
$io->writeln('<info>Main types</info>');
$printSection($typesTable['main']);
$io->writeln('<info>Relationships / Associations</info>');
$printSection($typesTable['relation']);
$io->writeln('<info>Array/Object Types</info>');
$printSection($typesTable['array_object']);
$io->writeln('<info>Date/Time Types</info>');
$printSection($typesTable['date_time']);
$io->writeln('<info>Other Types</info>');
// empty the values
$allTypes = array_map(function () {
return [];
}, $allTypes);
$printSection($allTypes);
}
private function createEntityClassQuestion(string $questionText): Question
{
$entityFinder = $this->fileManager->createFinder('app/Customize/Entity/')
// remove if/when we allow entities in subdirectories
->depth('<1')
->name('*.php');
$classes = [];
/** @var SplFileInfo $item */
foreach ($entityFinder as $item) {
if (!$item->getRelativePathname()) {
continue;
}
$classes[] = str_replace('/', '\\', str_replace('.php', '', $item->getRelativePathname()));
}
$question = new Question($questionText);
$question->setValidator([Validator::class, 'notBlank']);
$question->setAutocompleterValues($classes);
return $question;
}
private function askRelationDetails(ConsoleStyle $io, string $generatedEntityClass, string $type, string $newFieldName)
{
// ask the targetEntity
$targetEntityClass = null;
while (null === $targetEntityClass) {
$question = $this->createEntityClassQuestion('What class should this entity be related to?');
$targetEntityClass = $io->askQuestion($question);
if (!class_exists($targetEntityClass)) {
if (!class_exists($this->getEntityNamespace().'\\'.$targetEntityClass)) {
$io->error(sprintf('Unknown class "%s"', $targetEntityClass));
$targetEntityClass = null;
continue;
}
$targetEntityClass = $this->getEntityNamespace().'\\'.$targetEntityClass;
}
}
// help the user select the type
if ('relation' === $type) {
$type = $this->askRelationType($io, $generatedEntityClass, $targetEntityClass);
}
$askFieldName = function (string $targetClass, string $defaultValue) use ($io) {
return $io->ask(
sprintf('New field name inside %s', Str::getShortClassName($targetClass)),
$defaultValue,
function ($name) use ($targetClass) {
// it's still *possible* to create duplicate properties - by
// trying to generate the same property 2 times during the
// same make:entity run. property_exists() only knows about
// properties that *originally* existed on this class.
if (property_exists($targetClass, $name)) {
throw new \InvalidArgumentException(sprintf('The "%s" class already has a "%s" property.', $targetClass, $name));
}
return Validator::validateDoctrineFieldName($name, $this->doctrineHelper->getRegistry());
}
);
};
$askIsNullable = function (string $propertyName, string $targetClass) use ($io) {
return $io->confirm(sprintf(
'Is the <comment>%s</comment>.<comment>%s</comment> property allowed to be null (nullable)?',
Str::getShortClassName($targetClass),
$propertyName
));
};
$askOrphanRemoval = function (string $owningClass, string $inverseClass) use ($io) {
$io->text([
'Do you want to activate <comment>orphanRemoval</comment> on your relationship?',
sprintf(
'A <comment>%s</comment> is "orphaned" when it is removed from its related <comment>%s</comment>.',
Str::getShortClassName($owningClass),
Str::getShortClassName($inverseClass)
),
sprintf(
'e.g. <comment>$%s->remove%s($%s)</comment>',
Str::asLowerCamelCase(Str::getShortClassName($inverseClass)),
Str::asCamelCase(Str::getShortClassName($owningClass)),
Str::asLowerCamelCase(Str::getShortClassName($owningClass))
),
'',
sprintf(
'NOTE: If a <comment>%s</comment> may *change* from one <comment>%s</comment> to another, answer "no".',
Str::getShortClassName($owningClass),
Str::getShortClassName($inverseClass)
),
]);
return $io->confirm(sprintf('Do you want to automatically delete orphaned <comment>%s</comment> objects (orphanRemoval)?', $owningClass), false);
};
$askInverseSide = function (EntityRelation $relation) use ($io) {
if ($this->isClassInVendor($relation->getInverseClass())) {
$relation->setMapInverseRelation(false);
return;
}
// recommend an inverse side, except for OneToOne, where it's inefficient
$recommendMappingInverse = EntityRelation::ONE_TO_ONE !== $relation->getType();
$getterMethodName = 'get'.Str::asCamelCase(Str::getShortClassName($relation->getOwningClass()));
if (EntityRelation::ONE_TO_ONE !== $relation->getType()) {
// pluralize!
$getterMethodName = Str::singularCamelCaseToPluralCamelCase($getterMethodName);
}
$mapInverse = $io->confirm(
sprintf(
'Do you want to add a new property to <comment>%s</comment> so that you can access/update <comment>%s</comment> objects from it - e.g. <comment>$%s->%s()</comment>?',
Str::getShortClassName($relation->getInverseClass()),
Str::getShortClassName($relation->getOwningClass()),
Str::asLowerCamelCase(Str::getShortClassName($relation->getInverseClass())),
$getterMethodName
),
$recommendMappingInverse
);
$relation->setMapInverseRelation($mapInverse);
};
switch ($type) {
case EntityRelation::MANY_TO_ONE:
$relation = new EntityRelation(
EntityRelation::MANY_TO_ONE,
$generatedEntityClass,
$targetEntityClass
);
$relation->setOwningProperty($newFieldName);
$relation->setIsNullable($askIsNullable(
$relation->getOwningProperty(),
$relation->getOwningClass()
));
$askInverseSide($relation);
if ($relation->getMapInverseRelation()) {
$io->comment(sprintf(
'A new property will also be added to the <comment>%s</comment> class so that you can access the related <comment>%s</comment> objects from it.',
Str::getShortClassName($relation->getInverseClass()),
Str::getShortClassName($relation->getOwningClass())
));
$relation->setInverseProperty($askFieldName(
$relation->getInverseClass(),
Str::singularCamelCaseToPluralCamelCase(Str::getShortClassName($relation->getOwningClass()))
));
// orphan removal only applies if the inverse relation is set
if (!$relation->isNullable()) {
$relation->setOrphanRemoval($askOrphanRemoval(
$relation->getOwningClass(),
$relation->getInverseClass()
));
}
}
break;
case EntityRelation::ONE_TO_MANY:
// we *actually* create a ManyToOne, but populate it differently
$relation = new EntityRelation(
EntityRelation::MANY_TO_ONE,
$targetEntityClass,
$generatedEntityClass
);
$relation->setInverseProperty($newFieldName);
$io->comment(sprintf(
'A new property will also be added to the <comment>%s</comment> class so that you can access and set the related <comment>%s</comment> object from it.',
Str::getShortClassName($relation->getOwningClass()),
Str::getShortClassName($relation->getInverseClass())
));
$relation->setOwningProperty($askFieldName(
$relation->getOwningClass(),
Str::asLowerCamelCase(Str::getShortClassName($relation->getInverseClass()))
));
$relation->setIsNullable($askIsNullable(
$relation->getOwningProperty(),
$relation->getOwningClass()
));
if (!$relation->isNullable()) {
$relation->setOrphanRemoval($askOrphanRemoval(
$relation->getOwningClass(),
$relation->getInverseClass()
));
}
break;
case EntityRelation::MANY_TO_MANY:
$relation = new EntityRelation(
EntityRelation::MANY_TO_MANY,
$generatedEntityClass,
$targetEntityClass
);
$relation->setOwningProperty($newFieldName);
$askInverseSide($relation);
if ($relation->getMapInverseRelation()) {
$io->comment(sprintf(
'A new property will also be added to the <comment>%s</comment> class so that you can access the related <comment>%s</comment> objects from it.',
Str::getShortClassName($relation->getInverseClass()),
Str::getShortClassName($relation->getOwningClass())
));
$relation->setInverseProperty($askFieldName(
$relation->getInverseClass(),
Str::singularCamelCaseToPluralCamelCase(Str::getShortClassName($relation->getOwningClass()))
));
}
break;
case EntityRelation::ONE_TO_ONE:
$relation = new EntityRelation(
EntityRelation::ONE_TO_ONE,
$generatedEntityClass,
$targetEntityClass
);
$relation->setOwningProperty($newFieldName);
$relation->setIsNullable($askIsNullable(
$relation->getOwningProperty(),
$relation->getOwningClass()
));
$askInverseSide($relation);
if ($relation->getMapInverseRelation()) {
$io->comment(sprintf(
'A new property will also be added to the <comment>%s</comment> class so that you can access the related <comment>%s</comment> object from it.',
Str::getShortClassName($relation->getInverseClass()),
Str::getShortClassName($relation->getOwningClass())
));
$relation->setInverseProperty($askFieldName(
$relation->getInverseClass(),
Str::asLowerCamelCase(Str::getShortClassName($relation->getOwningClass()))
));
}
break;
default:
throw new \InvalidArgumentException('Invalid type: '.$type);
}
return $relation;
}
private function askRelationType(ConsoleStyle $io, string $entityClass, string $targetEntityClass)
{
$io->writeln('What type of relationship is this?');
$originalEntityShort = Str::getShortClassName($entityClass);
$targetEntityShort = Str::getShortClassName($targetEntityClass);
$rows = [];
$rows[] = [
EntityRelation::MANY_TO_ONE,
sprintf("Each <comment>%s</comment> relates to (has) <info>one</info> <comment>%s</comment>.\nEach <comment>%s</comment> can relate/has to (have) <info>many</info> <comment>%s</comment> objects", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort),
];
$rows[] = ['', ''];
$rows[] = [
EntityRelation::ONE_TO_MANY,
sprintf("Each <comment>%s</comment> relates can relate to (have) <info>many</info> <comment>%s</comment> objects.\nEach <comment>%s</comment> relates to (has) <info>one</info> <comment>%s</comment>", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort),
];
$rows[] = ['', ''];
$rows[] = [
EntityRelation::MANY_TO_MANY,
sprintf("Each <comment>%s</comment> relates can relate to (have) <info>many</info> <comment>%s</comment> objects.\nEach <comment>%s</comment> can also relate to (have) <info>many</info> <comment>%s</comment> objects", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort),
];
$rows[] = ['', ''];
$rows[] = [
EntityRelation::ONE_TO_ONE,
sprintf("Each <comment>%s</comment> relates to (has) exactly <info>one</info> <comment>%s</comment>.\nEach <comment>%s</comment> also relates to (has) exactly <info>one</info> <comment>%s</comment>.", $originalEntityShort, $targetEntityShort, $targetEntityShort, $originalEntityShort),
];
$io->table([
'Type',
'Description',
], $rows);
$question = new Question(sprintf(
'Relation type? [%s]',
implode(', ', EntityRelation::getValidRelationTypes())
));
$question->setAutocompleterValues(EntityRelation::getValidRelationTypes());
$question->setValidator(function ($type) {
if (!\in_array($type, EntityRelation::getValidRelationTypes())) {
throw new \InvalidArgumentException(sprintf('Invalid type: use one of: %s', implode(', ', EntityRelation::getValidRelationTypes())));
}
return $type;
});
return $io->askQuestion($question);
}
private function createClassManipulator(string $path, ConsoleStyle $io, bool $overwrite): ClassSourceManipulator
{
$manipulator = new ClassSourceManipulator($this->fileManager->getFileContents($path), $overwrite);
$manipulator->setIo($io);
return $manipulator;
}
private function getPathOfClass(string $class): string
{
return (new \ReflectionClass($class))->getFileName();
}
private function isClassInVendor(string $class): bool
{
$path = $this->getPathOfClass($class);
return $this->fileManager->isPathInVendor($path);
}
private function regenerateEntities(string $classOrNamespace, bool $overwrite, Generator $generator)
{
$regenerator = new EntityRegenerator($this->doctrineHelper, $this->fileManager, $generator, $overwrite);
$regenerator->regenerateEntities($classOrNamespace);
}
private function getPropertyNames(string $class): array
{
if (!class_exists($class)) {
return [];
}
$reflClass = new \ReflectionClass($class);
return array_map(function (\ReflectionProperty $prop) {
return $prop->getName();
}, $reflClass->getProperties());
}
private function doesEntityUseAnnotationMapping(string $className): bool
{
if (!class_exists($className)) {
$otherClassMetadatas = $this->doctrineHelper->getMetadata(Str::getNamespace($className).'\\', true);
// if we have no metadata, we should assume this is the first class being mapped
if (empty($otherClassMetadatas)) {
return false;
}
$className = reset($otherClassMetadatas)->getName();
}
$driver = $this->doctrineHelper->getMappingDriverForClass($className);
return $driver instanceof AnnotationDriver;
}
private function getEntityNamespace(): string
{
return $this->doctrineHelper->getEntityNamespace();
}
}
最後に下記のapp/Customize/Resources/help/MakeEntity.txtを設置してください。
The <info>%command.name%</info> command creates or updates an entity and repository class.
<info>php %command.full_name% BlogPost</info>
If the argument is missing, the command will ask for the entity class name interactively.
You can also mark this class as an API Platform resource. A hypermedia CRUD API will
automatically be available for this entity class:
<info>php %command.full_name% --api-resource</info>
You can also generate all the getter/setter/adder/remover methods
for the properties of existing entities:
<info>php %command.full_name% --regenerate</info>
You can also *overwrite* any existing methods:
<info>php %command.full_name% --regenerate --overwrite</info>
以上で完成です。
以下のコマンドを実行するとEntityとRepositoryを作成するウィザードが表示されます。
bin/console make:customize:entity
初回時Entityを作成するとエラーが表示されますが、もう一度同じEntityを作成するとフィールドを追加できるようになります。
投稿 EC-CUBE4のCustomizeディレクトリにEntityとRepositoryを自動生成するコマンドを用意する方法 は あずみ.net に最初に表示されました。