Using Doctrine ORM without symfony framework

There is a need to integrate Doctrine ORM and its entity manager into a custom coded PHP project, not based on symfony, i.e. not having symfony/framework nor symfony/orm-pack in it.

Let’s consider our previous project that uses Doctrine DBAL connection to insert scraped records into MySQL DB. Note that it does not have ORM, neither it has entity manager of Doctrine integrated. We’re using ::insert() method of Doctrine QueryBuilder. Let us remind that this project has the is a symfony console command using symfony/yaml and symfony/dependency-injection. Here is the current version of its composer dependencies:

"require": {
    "ext-json": "*",
    "symfony/dom-crawler": "^5.0",
    "symfony/css-selector": "^5.0",
    "doctrine/dbal": "^2.10",
    "symfony/console": "^5.0",
    "symfony/config": "^5.0",
    "symfony/dependency-injection": "^5.0",
    "symfony/yaml": "^5.0",
    "symfony/process": "^5.0",
    "monolog/monolog": "^2.0",
    "jakoch/phantomjs-installer": "^2.1"
},

Let’s replace “doctrine/dbal” with “doctrine/orm” (please use the latest stable version) first and reinstall the dependencies. Once done we need to generate Doctrine entities from the existing database using orm:convert-mapping command of doctrine console command. Note that after the composer update you should get doctrine executable file (PHP script) in your composer bin-dir. In our case it is ./bin, so the full command to launch will be:

./bin/doctrine orm:convert-mapping --from-database --namespace="App\Entity\" --force annotation src/

where –from-database option means you would like to connect to the existing DB and generate entities from the tables existing there. namespace option sets the desired namespace for all generated entities, so change it basing on your application needs. annotation option is the desired output of Doctrine entities config. We will be using annotations as default option nowadays in symfony, but you can also choose between yaml and xml as usual. And finally src/ is the target path to put the entities. They will be generated under the namespaced folders inside ./src, so in this case in ./src/App/Entity.

If you try to launch it now you will receive the following error message:

You are missing a "cli-config.php" or "config/cli-config.php" file in your
project, which is required to get the Doctrine Console working. You can use the
following sample as a template:
...

Which is obvious, because Doctrine doesn’t know how to connect to your DB, we never configured the DB connection for Doctrine ORM. So let’s create config-cli.php file in our config folder with the contents similar to the following:

<?php
 
use Doctrine\ORM\Tools\Console\ConsoleRunner;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Configuration;
 
$config = new Configuration();
$config->setProxyDir('Proxies');
$config->setProxyNamespace('App\Proxies');
 
$paths = [
    'App\\' => realpath(__DIR__) . "/../src/",
];
 
// Tells Doctrine what mode we want
$isDevMode = true;
 
// Doctrine connection configuration
$dbParams = array(
    'driver'   => 'pdo_mysql',
    'user'     => 'root',
    'password' => 'password',
    'dbname'   => 'database',
    'host'     => 'db'
);
 
$driverImpl = $config->newDefaultAnnotationDriver($paths, false);
$config->setMetadataDriverImpl($driverImpl);
$entityManager = EntityManager::create($dbParams, $config);
 
return ConsoleRunner::createHelperSet($entityManager);

and re-run the command again. Assuming we have three tables in our database: product, category and manufacturer, we should receive an output like:

Processing entity "App\Entity\Category"
Processing entity "App\Entity\Manufacturer"
Processing entity "App\Entity\Product"
Exporting "annotation" mapping information to "/path/to/your/project/src"

and you should see the entities classes created under ./src/App/Entity folder. Note however, that there are no Doctrine relations unless you specified foreign keys constraints when you created your tables. So you should set the relations manually in the entities like an example below:

in Category.php:

use Doctrine\Common\Collections\ArrayCollection;
...
/**
 * @var ArrayCollection
 *
 * @ORM\OneToMany(targetEntity="Product", mappedBy="category")
 */
private $products;

in Manufacturer.php:

use Doctrine\Common\Collections\ArrayCollection;
...
/**
 * @var ArrayCollection
 *
 * @ORM\OneToMany(targetEntity="Product", mappedBy="manufacturer")
 */
private $products;

and finally in Product.php:

/**
 * @var Category
 *
 * @ORM\ManyToOne(targetEntity="Category", inversedBy="products")
 * @ORM\JoinColumn(name="category_id", referencedColumnName="id")
 */
private $category;
 
/**
 * @var Manufacturer|null
 *
 * @ORM\ManyToOne(targetEntity="Manufacturer", inversedBy="products")
 * @ORM\JoinColumn(name="manufacturer_id", referencedColumnName="id")
 */
private $manufacturer;

and then you need to regenerate your entities again for doctrine console command to add getters/setters for the newly created relations. Note also that these changes replace the old category_id and manufactuer_id columns in Product entity. To regenerate entities launch:

./bin/doctrine orm:generate-entities src/

with the cli-config.php Doctrine knows the namespace under the ./src path to find the entities, so you should receive a successful message like:

Processing entity "App\Entity\Category"
Processing entity "App\Entity\Product"
Processing entity "App\Entity\Manufacturer"
[OK] Entity classes generated to "/scraper/src"

and you should see your newly generated getters and setters for dependent entities of Product:

/**
 * Set category.
 *
 * @param \App\Entity\Category|null $category
 *
 * @return Product
 */
public function setCategory(\App\Entity\Category $category = null)
{
    $this->category = $category;
 
    return $this;
}
 
/**
 * Get category.
 *
 * @return \App\Entity\Category|null
 */
public function getCategory()
{
    return $this->category;
}

Now it’s time to get back to the application code, where we need to replace DBAL and QueryBuilder with instantiating new entities. First of all, we need to pass an instance of Doctrine EntityManager into the constructor of our command and assign it to the class property. Let’s replace the old DBAL property called $connection with a new one expecting an instance of EntityManagerInterface and called $em:

use Doctrine\ORM\EntityManagerInterface;
...
class ScraperCommand extends Command {
...
    /**
     * @var EntityManagerInterface
     */
     private $em;
...
     public function __construct(
         ParameterBagInterface $parameters,
         Logger $logger,
         PhantomJS $phantomJS,
         EntityManagerInterface $em,
         string $name = null
    ) {
     parent::__construct($name);
 
     $this->parameters = $parameters;
     $this->logger = $logger;
     $this->phantomJS = $phantomJS;
     $this->em = $em;
  }
}

Now it’s time to implement the logic. In our case (just an example) we will check if the category and manufacturer with the given name exist and if not we will create new entities and persist them. Same with product searching by title and manufacturer:

if (!empty($categoryName)) {
  $category = $this->em->getRepository('App\Entity\Category')->findOneBy(
      [
          'name' => $categoryName
      ]
  );
  if (!$category) {
    $category = new Category();
    $category->setName($categoryName);
 
    $this->em->persist($category);
    $this->em->flush();
  }
}
...
if (!empty($item['title'][0])) {
$product = $this->em->getRepository('App\Entity\Product')->findOneBy(
[
'title' => $item['title'][0],
 'manufacturer' => $manufacturer ?? null
]
);
 
 if (!$product) {
$product = new Product();
 
 $product->setTitle($item['title'][0]);
 $product->setPrice($item['price'][0]);
 $product->setManufacturer($manufacturer ?? null);
 $product->setCategory($category ?? null);
 $product->setRating($item['rating'][0]);
 
 $this->em->persist($product);
 $this->em->flush();
 }
}

That’s it, you can see the PR with the changes described here.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.