Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Discussion options

Hello community, I have a question:
Why don't we have the "doctrine:mapping:import" option in Symfony 7 anymore? That reverse engineering tool was very useful. 😢

What alternatives are there?
Cheers!

You must be logged in to vote

doctrine:mapping:import

Hi,

Explanations

Unfortunately, this was dropped (by Doctrine beforehand, not by Symfony, btw) since version 5. The reason, as you would believe it to be, is that it was mostly uneffective, disappointing for many people as it could not reflect the complexity of database structures, and needed to follow all the updates from the different DBMS, which, at this point, is starting to be a little insane (it's already hard to tackle all the issues in the DBAL section).

Note: the latest documentation was here: https://www.doctrine-project.org/projects/doctrine-orm/en/2.19/reference/tools.html#reverse-engineering

Benjamin summed up a custom approach here: doctrine/orm#112…

Replies: 2 comments · 3 replies

Comment options

I share my own solution for Postgres

use php bin/console app:importTableToEntity --schema="public" --table="my_table"

<?php

namespace App\Command\ImportTableToEntity;

use AllowDynamicProperties;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Persistence\ManagerRegistry;
use Exception;
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\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\HttpKernel\KernelInterface;

 #[AsCommand(
    name: 'app:importTableToEntity',
    description: 'Add a short description for your command',
)]
class ImportTableToEntityCommand extends Command
{



    private string $projectDir;
    private EntityManager $entityManager;

    public function __construct( KernelInterface $kernel,ManagerRegistry $doctrine)
    {

        $this->projectDir                  = $kernel->getProjectDir();
        $this->entityManager               = $doctrine->getManager('map');

        parent::__construct();
    }

    protected function configure(): void
    {
        $this

            ->addOption('schema', null, InputOption::VALUE_REQUIRED, 'Option schema')
            ->addOption('table', null, InputOption::VALUE_REQUIRED, 'Option table')
        ;
    }

    protected function execute(InputInterface $input, OutputInterface $output): int
    {
        $io = new SymfonyStyle($input, $output);
        $schema = $input->getOption('schema');
        $table  = $input->getOption('table');

        if ($schema=='') {
            $io->error('Argument "schema" is required.');
            return Command::FAILURE;
        }

        if ($table=='') {
            $io->error('Argument "table" is required.');
            return Command::FAILURE;
        }

        try {
            $this->generateEntity($io,$schema, $table);
        } catch (Exception $e) {
            $io->error("ERROR CATCH ->".$e->getMessage());
            return Command::FAILURE;
        }




        return Command::SUCCESS;
    }

    function generateEntity(SymfonyStyle $io,string $schema, string $tableName): void
    {

        $columns            = $this->getColumns($schema, $tableName);
        $foreignKeys        = $this->getForeignKeys($schema, $tableName);
        $sequences          = $this->getSequences($schema, $tableName);
        $indexes            = $this->getIndexes($schema, $tableName);
        $uniqueConstraints  = $this->getUniqueConstraints($schema, $tableName);


        if(count($columns)==0){
            $io->warning('table ->'.$schema.".".$tableName." Not Exist");
            return;
        }


        $className = ucfirst($schema).$this->snakeToPascalCase($tableName);
        $entityCode = "<?php\n\n";
        $entityCode .= "namespace App\\EntityTest;\n\n";
        $entityCode .= "use Doctrine\\ORM\\Mapping as ORM;\n\n";
        $entityCode .= "use Doctrine\\DBAL\\Types\\Types;\n\n";
        $entityCode .= "#[ORM\\Table(name: '$schema.$tableName')]\n";

        // índices
        foreach ($indexes as $indexName => $index) {
            $columnsIndex = implode("', '", $index['columns']);
            $isUnique = $index['is_unique'] ? ', unique: true' : '';
            if (!$index['is_primary']) {
                $entityCode .= "#[ORM\\Index(name: '$indexName', columns: ['$columnsIndex'] $isUnique)]\n";
            }
        }

        //uniques
        foreach ($uniqueConstraints as $constraintName => $columnsUniq) {
            $columnsList = implode("', '", $columnsUniq);
            $entityCode .= "#[ORM\\UniqueConstraint(name: '$constraintName', columns: ['$columnsList'])]\n";
        }


        $entityCode .= "#[ORM\\Entity(repositoryClass: '".ucfirst($schema).$this->snakeToPascalCase($tableName)."Repository')]\n";
        $entityCode .= "class $className\n{\n";





        foreach ($columns as $column) {
            $propertyName = lcfirst(str_replace('_', '', ucwords($column['column_name'], '_')));
            $type  = $this->mapColumnTypeToPhp($column['data_type']);
            $types = $this->mapColumnTypeToPhpTypes($column['data_type']);
            $nullable = $column['is_nullable'] === 'YES' ? 'true' : 'false';


            if(!array_key_exists($column['column_name'], $foreignKeys)){
                if (array_key_exists($column['column_name'], $sequences)) {
                    $sequenceName = $sequences[$column['column_name']]['schema'] . '.' . $sequences[$column['column_name']]['sequence_name'];
                    $entityCode .= "    #[ORM\\Id]\n";
                    $entityCode .= "    #[ORM\\GeneratedValue(strategy: 'IDENTITY')]\n";
                    $entityCode .= "    #[ORM\\SequenceGenerator(sequenceName: '$sequenceName', allocationSize: 1, initialValue: 1)]\n";
                    $entityCode .= "    #[ORM\\Column(name: '{$column['column_name']}',type: $types, nullable: $nullable)]\n";
                    $entityCode .= "    private ?$type \$$propertyName = null;\n\n";
                } else {
                        $entityCode .= "    #[ORM\\Column(name: '{$column['column_name']}', type: $types, nullable: $nullable)]\n";
                        if($column['column_name']=='id'){
                            $entityCode .= "    #[ORM\Id]\n";
                        }
                        $entityCode .= "    private ?$type \$$propertyName = null;\n\n";
                }
            }



        }

        foreach ($foreignKeys as $columnName => $foreignKey) {
            $propertyName = lcfirst(str_replace('_', '', ucwords($columnName, '_')));
            $referencedSchema = $foreignKey['referenced_schema'];
            $referencedTable = $foreignKey['referenced_table'];
            $referencedEntity = ucfirst($referencedSchema).$this->snakeToPascalCase($referencedTable);

            $entityCode .= "    #[ORM\\ManyToOne(targetEntity: $referencedEntity::class)]\n";
            $entityCode .= "    #[ORM\\JoinColumn(name: '$columnName', referencedColumnName: '{$foreignKey['referenced_column']}', nullable: true)]\n";
            $entityCode .= "    private ?$referencedEntity \$$propertyName =null;\n\n";

        }


        $entityCode .= "    \n\n";
        $entityCode .= "    public function __construct()\n";
        $entityCode .= "    {\n\n";
        $entityCode .= "    }\n\n";
        $entityCode .= "    public function __toString()\n";
        $entityCode .= "    {\n";
        $entityCode .= "        return json_encode(array(\n\n";
        foreach ($columns as $k=> $column) {
            $propertyName = lcfirst(str_replace('_', '', ucwords($column['column_name'], '_')));
            $entityCode .= "       '{$column['column_name']}'=>\$this->{$propertyName}";
            if(($k+1)<count($columns)) {
                $entityCode .= ",";
            }
            $entityCode .= "\n";
        }
        $entityCode .= "        ));\n";
        $entityCode .= "    }\n\n";

        $entityCode .= "}\n";


        file_put_contents($this->projectDir.'/src/EntityTest/'.$className.'.php', $entityCode);
        $io->success("Entity $className generated");
    }

    function getColumns(string $schema, string $tableName): array
    {

        $query = "
        SELECT
            column_name,
            data_type,
            is_nullable
        FROM
            information_schema.columns
        WHERE
            table_schema = :schema
            AND table_name = :table
    ";

        $rsm = new ResultSetMapping();
        $rsm->addScalarResult('column_name', 'column_name');
        $rsm->addScalarResult('data_type', 'data_type');
        $rsm->addScalarResult('is_nullable', 'is_nullable');


        $query = $this->entityManager->createNativeQuery($query, $rsm);
        $query->setParameter("schema", $schema);
        $query->setParameter("table", $tableName);
        $result= $query->getResult();

        return $result;
    }

    function getForeignKeys(string $schema, string $tableName): array
    {


        $query = "
        SELECT
            kcu.column_name,
            ccu.table_schema AS referenced_schema,
            ccu.table_name AS referenced_table,
            ccu.column_name AS referenced_column
        FROM
            information_schema.key_column_usage kcu
        JOIN
            information_schema.table_constraints tc
            ON kcu.constraint_name = tc.constraint_name
            AND kcu.table_schema = tc.table_schema
        JOIN
            information_schema.constraint_column_usage ccu
            ON ccu.constraint_name = tc.constraint_name
        WHERE
            tc.constraint_type = 'FOREIGN KEY'
            AND kcu.table_schema = :schema
            AND kcu.table_name = :table
    ";


        $rsm = new ResultSetMapping();
        $rsm->addScalarResult('column_name', 'column_name');
        $rsm->addScalarResult('referenced_schema', 'referenced_schema');
        $rsm->addScalarResult('referenced_table', 'referenced_table');
        $rsm->addScalarResult('referenced_column', 'referenced_column');


        $query = $this->entityManager->createNativeQuery($query, $rsm);
        $query->setParameter("schema", $schema);
        $query->setParameter("table", $tableName);
        $result= $query->getResult();


        $foreignKeys = [];
        foreach ($result as $fk) {
            $foreignKeys[$fk['column_name']] = [
                'referenced_schema' => $fk['referenced_schema'],
                'referenced_table' => $fk['referenced_table'],
                'referenced_column' => $fk['referenced_column'],
            ];
        }

        return $foreignKeys;
    }

    function getSequences(string $schema,string $tableName): array
    {
        global $pdo;

        $query = "
     SELECT
            c.column_name,
            pgc.relname AS sequence_name,
            c.table_schema
        FROM
            information_schema.columns c
        JOIN
            pg_attrdef ad
            ON ad.adrelid = (c.table_schema || '.' || c.table_name)::regclass::oid
            AND ad.adnum = c.ordinal_position
        JOIN
            pg_class pgc
            ON pgc.oid = substring(pg_get_expr(ad.adbin, ad.adrelid) FROM 'nextval\\(''(.*?)''')::regclass
        WHERE
            c.table_schema = :schema
            AND c.table_name = :table
            AND c.column_default LIKE 'nextval(%'
    ";

        $rsm = new ResultSetMapping();
        $rsm->addScalarResult('column_name', 'column_name');
        $rsm->addScalarResult('sequence_name', 'sequence_name');
        $rsm->addScalarResult('table_schema', 'table_schema');


        $query = $this->entityManager->createNativeQuery($query, $rsm);
        $query->setParameter("schema", $schema);
        $query->setParameter("table", $tableName);
        $result= $query->getResult();

        $sequences = [];
        foreach ($result as $sequence) {
            $sequences[$sequence['column_name']] = [
                'sequence_name' => $sequence['sequence_name'],
                'schema' => $sequence['table_schema']
            ];
        }

        return $sequences;
    }

    function getIndexes(string $schema, string $tableName): array
    {

        $query = "
        SELECT
            i.relname AS index_name,
            a.attname AS column_name,
            ix.indisunique AS is_unique,
            ix.indisprimary AS is_primary
        FROM
            pg_class t
        JOIN
            pg_index ix ON t.oid = ix.indrelid
        JOIN
            pg_class i ON i.oid = ix.indexrelid
        JOIN
            pg_attribute a ON a.attnum = ANY(ix.indkey) AND a.attrelid = t.oid
        WHERE
            t.relname = :table
            AND t.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = :schema)
            AND ix.indisunique = 'f'  -- Esto asegura que solo traemos índices normales, no únicos
    ";
        $rsm = new ResultSetMapping();
        $rsm->addScalarResult('index_name', 'index_name');
        $rsm->addScalarResult('column_name', 'column_name');
        $rsm->addScalarResult('is_unique', 'is_unique');
        $rsm->addScalarResult('is_primary', 'is_primary');


        $query = $this->entityManager->createNativeQuery($query, $rsm);
        $query->setParameter("schema", $schema);
        $query->setParameter("table", $tableName);
        $result= $query->getResult();

        $indexes = [];
        foreach ($result as $index) {
            $indexName = $index['index_name'];
            if (!isset($indexes[$indexName])) {
                $indexes[$indexName] = [
                    'columns' => [],
                    'is_unique' => $index['is_unique'] ,
                    'is_primary' => $index['is_primary'] ,
                ];
            }
            $indexes[$indexName]['columns'][] = $index['column_name'];
        }

        return $indexes;
    }

    function getUniqueConstraints(string $schema, string $tableName): array
    {

        $query = "
       

         SELECT
            i.relname AS constraint_name,
            a.attname AS column_name
        FROM
            pg_class t
        JOIN
            pg_index ix ON t.oid = ix.indrelid
        JOIN
            pg_class i ON i.oid = ix.indexrelid
        JOIN
            pg_attribute a ON a.attnum = ANY(ix.indkey) AND a.attrelid = t.oid
        WHERE
            t.relname = :table
            AND t.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = :schema)  
            AND i.relname = (
                SELECT DISTINCT
                    conname AS constraint_name
                FROM
                    pg_constraint c
                JOIN
                    pg_attribute a ON a.attnum = ANY(c.conkey)
                WHERE
                    c.contype = 'u'  -- Filtramos solo las restricciones de unicidad
                    AND c.connamespace = (SELECT oid FROM pg_namespace WHERE nspname = :schema)  
                    AND c.conrelid = (
                        SELECT oid
                        FROM pg_class
                        WHERE relname = :table
                        AND relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = :schema)  
                    )
            );
    ";
        $rsm = new ResultSetMapping();
        $rsm->addScalarResult('constraint_name', 'constraint_name');
        $rsm->addScalarResult('column_name', 'column_name');



        $query = $this->entityManager->createNativeQuery($query, $rsm);
        $query->setParameter("schema", $schema);
        $query->setParameter("table", $tableName);
        $result= $query->getResult();


        $constraints = [];
        foreach ($result as $row) {
            $constraints[$row['constraint_name']][] = $row['column_name'];
        }

        return $constraints;
    }



    function mapColumnTypeToPhp(string $dbType): string
    {
        return match ($dbType) {
            'integer', 'smallint', 'bigint'                                     => 'int',
            'real', 'double precision', 'decimal'                               => 'float',
            'numeric'                                                           => 'string',
            'character varying', 'text', 'char'                                 => 'string',
            'boolean'                                                           => 'bool',
            'timestamp without time zone', 'timestamp with time zone'           => 'DateTimeInterface',
            'date'                                                              => 'DateTimeInterface',
            'time without time zone' , 'time with time zone'                    => 'DateTimeInterface',
            default => 'mixed',
        };
    }

    function mapColumnTypeToPhpTypes(string $dbType): string
    {
        return match ($dbType) {
            'integer', 'smallint', 'bigint'                                     => 'Types::INTEGER',
            'real', 'double precision', 'decimal'                               => 'Types::FLOAT',
            'numeric'                                                           => 'Types::DECIMAL',
            'character varying', 'text', 'char'                                 => 'Types::STRING',
            'boolean'                                                           => 'Types::BOOLEAN',
            'timestamp without time zone', 'timestamp with time zone'           => 'Types::DATETIME_MUTABLE',
            'date'                                                              => 'Types::DATE_MUTABLE',
            'time without time zone' , 'time with time zone'                    => 'Types::TIME_MUTABLE',
            default => 'mixed',
        };
    }

    function snakeToPascalCase(string $snakeCase): string {
        $snakeCase = str_replace('_', ' ', $snakeCase);
        $pascalCase = ucwords($snakeCase);
        return str_replace(' ', '', $pascalCase);
    }




}
You must be logged in to vote
1 reply
@fabifaure
Comment options

Works fine on Symfony 8 👍
Just add this last script in a src/command folder
and add it as a service for the command to be indexed :
In services.yalm you can add the following code :
App\Command\ImportTableToEntity\ImportTableToEntityCommand:
tags:
- { name: 'console.command', command: 'app:importTableToEntity' }

I will modify it to fetch all the table for a given schema, keep you in touch

SEQUENCES are not replicated in model...
You will have to modify the sequence request by this new one :
$query = "
SELECT
c.column_name,
pg_get_serial_sequence(:schema || '.' || :table, c.column_name) sequence_name,
c.table_schema
FROM
information_schema.columns c
WHERE
c.table_schema = :schema
AND c.table_name = :table
AND pg_get_serial_sequence(:schema || '.' || :table, c.column_name) IS NOT NULL
";

Good reverse engineering for those dealing with big databases!

Comment options

doctrine:mapping:import

Hi,

Explanations

Unfortunately, this was dropped (by Doctrine beforehand, not by Symfony, btw) since version 5. The reason, as you would believe it to be, is that it was mostly uneffective, disappointing for many people as it could not reflect the complexity of database structures, and needed to follow all the updates from the different DBMS, which, at this point, is starting to be a little insane (it's already hard to tackle all the issues in the DBAL section).

Note: the latest documentation was here: https://www.doctrine-project.org/projects/doctrine-orm/en/2.19/reference/tools.html#reverse-engineering

Benjamin summed up a custom approach here: doctrine/orm#11245 (comment)
Latest known code before removal can still be found in ^2.0 (2.7.3):

Of course, you could revert (temporarily) to DoctrineBundle 2.0.x, but I suggest you try the command yourself instead.

Solutions

You could:

        <!-- vendor/doctrine/doctrine-bundle/config/orm.xml, just check it's still here around line 276 -->
        <service id="doctrine.mapping_import_command" class="Doctrine\Bundle\DoctrineBundle\Command\ImportMappingDoctrineCommand">
            <argument type="service" id="doctrine" />
            <argument>%kernel.bundles%</argument>

            <tag name="console.command" command="doctrine:mapping:import" />
        </service>
  • Mark the command explicitely as a Command (not necessary though)
// vendor/doctrine/doctrine-bundle/src/Command/ImportMappingDoctrineCommand.php#27
// Add this and remove the @deprecated comment
#[AsCommand(name: 'doctrine:mapping:import')]
class ImportMappingDoctrineCommand extends DoctrineCommand
{
        // vendor/doctrine/doctrine-bundle/src/DependencyInjection/DoctrineExtension.php#576
        // Remove/comment those lines
        if (! class_exists(ClassMetadataExporter::class)) {
            $container->removeDefinition('doctrine.mapping_import_command');
        }
  • Delete the cache (like var/cache directory) or php bin/console cache:clear --warmup, I think some of this is cacheable by PHP / Opcache
  • Call it again via php bin/console doctrine:mapping:import [args] and see what happens.

Lazy way :)

Also, @ebootingdev, I think you should check this tool: https://github.com/siburuxue/doctrine-helper ;)

There you go!

You must be logged in to vote
2 replies
@ebootingdev
Comment options

Thank you, esteemed one. 😊

@DocFX
Comment options

You can mark it as a solution if that's what you were searching for! That might help other people to identify your topic as a resourceful one! 😊

Otherwise, happy to help more if I can!

Answer selected by ebootingdev
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Category
🙏
Q&A
Labels
None yet
3 participants
Morty Proxy This is a proxified and sanitized view of the page, visit original site.