2

Alright so I'm converting a small laravel project to symfony (will get bigger, and the bundling architecture symfony uses will be ideal)

I'm apparently spoiled with laravels facades and eloquent working with existing databases almost right out of the box.

I can't find the most appropriate way to have a wrapper or "helper" class get access to an entities repository.

first let me give a few examples then I will explain what I have attempted. (I'm willing to bounty some points for a good answer but unfortunately the time constraints on the project can't exactly wait)

So in laravel I had all my model classes. Then I created some wrapper / helper classes that would essentially turn the data into something a little more usable (i.e. multiple queries and objects containing more versatile information to work with). And with the magic of facades I could call upon each model and query them without and dependencies injected into these "Helper" classes. keeping them very lean. In symfony it appears the ideal solution is to put all of your reusable database logic in repositories, ok.

In symfony I'm surrounded by Inversion of Control (IoC); which is fine but design pattern is failing to be intuitive for me to fully figure this scenario out. I have tried to create services out every single repository, which works great if being called from a controller or other Dependency Injected (DI) service. But in a standard php class, it appears my hands are tied without passing entity manager to each helper class's constructor. *shivers*

The first limitation is I have zero ability to change the schema of the existing tables (which obviously doesn't change the problem, just don't want anyone to suggest altering the entities).

So how does one accomplish this.

EDIT:

so thanks to @mojo's comment I've pulled off what I wanted to do. Still looking for a better alternative if it exists. (see edit 2 below)

currently I have:

config.yml docterine.orm.entity_managers:

entity_managers:
     default:
         auto_mapping: true
         connection: default
     asterisk:
         connection: asterisk
         mappings:
             AsteriskDbBundle:  ~
     asteriskcdr:
         connection: asteriskcdr
         mappings:
             AsteriskCdrDbBundle:

service.yml

services:
    app.services.doctrine.entitymanager.provider:
        class: AppBundle\Services\EntityManagerProvider
        arguments: [@doctrine]
        tags:
             - {name: kernel.event_listener, event: kernel.request, method: onKernelRequest}

EntityManagerProvider

namespace AppBundle\Services;
use Doctrine\Bundle\DoctrineBundle\Registry as DoctrineRegistry;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Config\Definition\Exception\Exception;

class EntityManagerProvider
{
    /** @var  DoctrineRegistry */
    private static $doctrine;

    public function __construct(DoctrineRegistry $doctrine)
    {
        static::$doctrine = $doctrine;
    }

    /**
     * @param $class
     * @return EntityManager
     */
    public static function getEntityManager($class)
    {
        if(($em = static::$doctrine->getManagerForClass($class)) instanceof EntityManager == false)
            throw new Exception(get_class($em) . ' is not an instance of ' . EntityManager::class);

        return $em;
    }

    // oh man does this feel dirty
    public function onKernelRequest($event)
    {
        return;
    }
}

Example Controller

$extension = Extension::createFromDevice(DeviceRepository::findById(92681));

ExtendedEntityRepository

namespace AppBundle\Entity;
use AppBundle\Services\EntityManagerProvider;
use AppBundle\Utils\DateTimeRange;
use Doctrine\DBAL\Query\QueryBuilder;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\ClassMetadata;
use Symfony\Component\Config\Definition\Exception\Exception;

class ExtendedEntityRepository extends \Doctrine\ORM\EntityRepository
{
    /** @var  ExtendedEntityRepository */
    protected static $instance;

    public function __construct(EntityManager $entityManager, ClassMetadata $class)
    {
        parent::__construct($entityManager, $class);

        if(static::$instance instanceof static == false)
            static::$instance = $this;
    }

    // some horribly dirty magic to get the entity that belongs to this repo... which requires the repos to have the same name and exist one directory down in a 'Repositories' folder
    public static function getInstance()
    {
        if(static::$instance instanceof static == false) {
            preg_match('/^(.*?)Repositories\\\([A-Za-z_]*?)Repository$/', static::class, $match);
            $class = $match[1] . $match[2];
            $em = EntityManagerProvider::getEntityManager($class);
            static::$instance = new static($em, $em->getClassMetadata($class));
        }
        return static::$instance;
    }

    public static function findById($id)
    {
        return static::getInstance()->find($id);
    }

    public static function getQueryBuilder()
    {
        return static::getInstance()->getEntityManager()->createQueryBuilder();
    }

    public static function getPreBuiltQueryBuilder()
    {
        return static::getQueryBuilder()->select('o')->from(static::getInstance()->getClassName(), 'o');
    }

    public static function findByColumn($column, $value)
    {
        //if($this->getClassMetadata()->hasField($column) == false)
        //    throw new Exception($this->getEntityName() . " does not contain a field named `{$column}`");
        return static::getPreBuiltQueryBuilder()->where("{$column} = ?1")->setParameter(1, $value)->getQuery()->execute();
    }

    public static function filterByDateTimeRange($column, DateTimeRange $dateTimeRange, QueryBuilder $queryBuilder = null)
    {
        if($queryBuilder == null)
            $queryBuilder = static::getPreBuiltQueryBuilder();
        if($dateTimeRange != null && $dateTimeRange->start instanceof \DateTime && $dateTimeRange->end instanceof \DateTime) {
            return $queryBuilder->andWhere(
            $queryBuilder->expr()->between($column, ':dateTimeFrom', ':dateTimeTo')
            )->setParameters(['dateTimeFrom' => $dateTimeRange->start, 'dateTimeTo' => $dateTimeRange->end]);
        }
        return $queryBuilder;
    }
}

DeviceRepository

namespace Asterisk\DbBundle\Entity\Repositories;
use AppBundle\Entity\ExtendedEntityRepository;

/**
 * DeviceRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class DeviceRepository extends ExtendedEntityRepository
{
    //empty as it only needs to extend the ExtendedEntityRepository class
}

Extension

namespace AppBundle\Wrappers;

use Asterisk\DbBundle\Entity\Device;

class Extension
{
    public $displayName;
    public $number;

    public function __construct($number, $displayName = "")
    {
        $this->number = $number;
        $this->displayName = $displayName;
    }

    public static function createFromDevice(Device $device)
    {
        return new Extension($device->getUser(), $device->getDescription());
    }
}

Agent (This is an example of why having repositories access statically is helpful)

namespace AppBundle\Wrappers;


use AppBundle\Utils\DateTimeRange;
use Asterisk\CdrDbBundle\Entity\Cdr;
use Asterisk\CdrDbBundle\Entity\Repositories\CdrRepository;
use Asterisk\DbBundle\Entity\Device;
use Asterisk\DbBundle\Entity\Repositories\FeatureCodeRepository;
use Asterisk\DbBundle\Entity\Repositories\QueueDetailRepository;
use Asterisk\DbBundle\Enums\QueueDetailKeyword;

class Agent
{
    public $name;

    public $extension;

    /** @var  Call[] */
    public $calls = [];

    /** @var array|Queue[] */
    public $queues = [];

    /** @var AgentStats  */
    public $stats;

    private $_extension;

    public function __construct(Device $extension, DateTimeRange $dateTimeRange = null)
    {
        $this->_extension = $extension;
        $this->extension = Extension::createFromDevice($extension);
        $this->name = $this->extension->displayName;
        $this->calls = $this->getCalls($dateTimeRange);
        $this->stats = new AgentStats($this, $dateTimeRange);
    }

    public function getCalls(DateTimeRange $dateTimeRange = null)
    {
        /** @var CdrRepository $cdrRepo */
        $cdrRepo = CdrRepository::getPreBuiltQueryBuilder();
        $query = $cdrRepo->excludeNoAnswer($cdrRepo->filterByDateTimeRange($dateTimeRange));

        $cdrs = $query->andWhere(
                $query->expr()->orX(
                    $query->expr()->eq('src', $this->extension->number),
                    $query->expr()->eq('dst', $this->extension->number)
                )
            )->andWhere(
                $query->expr()->notLike('dst', '*%')
            )
            ->getQuery()->execute();

        foreach($cdrs as $cdr) {
            $this->calls[] = new Call($cdr);
        }
        return $this->calls;
    }

    public function getBusyRange(DateTimeRange $dateTimeRange = null)
    {
        $on = FeatureCodeRepository::getDndActivate();
        $off = FeatureCodeRepository::getDndDeactivate();
        $toggle = FeatureCodeRepository::getDndToggle();

        $query = CdrRepository::filterByDateTimeRange($dateTimeRange);

        /** @var Cdr[] $dndCdrs */
        $dndCdrs = $query->where(
                    $query->expr()->in('dst', [$on, $off, $toggle])
                )
                ->where(
                    $query->expr()->eq('src', $this->extension->number)
                )->getQuery()->execute();

        $totalTimeBusy = 0;

        /** @var \DateTime $lastMarkedBusy */
        $lastMarkedBusy = null;
        foreach($dndCdrs as $cdr) {
            switch($cdr->getDst())
            {
            case $on:
                $lastMarkedBusy = $cdr->getDateTime();
                break;
            case $off:
                if($lastMarkedBusy != null)
                    $totalTimeBusy += $lastMarkedBusy->diff($cdr->getDateTime());
                $lastMarkedBusy = null;
                break;
            case $toggle:
                if($lastMarkedBusy == null) {
                    $lastMarkedBusy = $cdr->getDateTime();
                }
                else
                {
                    $totalTimeBusy += $lastMarkedBusy->diff($cdr->getDateTime());
                    $lastMarkedBusy = null;
                }
                break;
            }
        }
        return $totalTimeBusy;
    }

    public function getQueues()
    {
        $query = QueueDetailRepository::getPreBuiltQueryBuilder();
        $queues = $query->where(
                $query->expr()->eq('keyword', QueueDetailKeyword::Member)
            )->where(
                $query->expr()->like('data', 'Local/'.$this->extension->number.'%')
            )->getQuery()->execute();

        foreach($queues as $queue)
            $this->queues[] = Queue::createFromQueueConfig(QueueDetailRepository::findByColumn('extension', $queue->id), $queue);
        return $this->queues;
    }
}

EDIT 2:

Actually I forgot I declared each repository as a service, so I could omit the black magic voodoo in the getInstance() method. But loading the service on kernel event seems like a bad idea...

parameters:
    entity.device: Asterisk\DbBundle\Entity\Device
services:
    asterisk.repository.device:
    class: Asterisk\DbBundle\Entity\Repositories\DeviceRepository
    factory: ["@doctrine.orm.asterisk_entity_manager", getRepository]
    arguments:
        - %entity.device%
    tags:
        - {name: kernel.event_listener, event: kernel.request, method: onKernelRequest}

Edit 3

Cerad gave me an answer on my other related question That suggested using a single kernel event listener service and injecting each repository as a dependency. Thus allowing me to access the repositories statically. My only concern is the overhead required to load each repository on every request. My ideal method would be lazy load the repositories, but I'm unaware of a method at this time. proxy-manager-bridge looked promising but with my singleton pattern I don't think it will work.

Community
  • 1
  • 1
StrikeForceZero
  • 2,379
  • 1
  • 25
  • 36
  • 2
    I can't seem to understand what EXACTLY is your problem. In Symfony the `EntityRepository` class is responsible **only** for the queries. It shouldn't be doing anything else than retrieving the data from your database(or source). If you need to further manipulate the data (i.e. convert it into an array) you can create a class with static methods which handles that. Anyway, without any code it's kind of hard to see what you're trying to do. – tftd Oct 14 '15 at 00:54
  • 2
    First off, it's extremely irresponsible and dangerous to run out a door while typing. What if you ran into an innocent bystander? Secondly, I think you may be having trouble making the transition from Laravel's active record approach and doctrine's orm approach. Consider sitting down, or at least be stationary, and creating a new question about a specific issue. – Cerad Oct 14 '15 at 01:23
  • at least you gave me a good laugh. I'll see if I can elaborate more on my question when the fog clears. Although I disagree is has anything to do with going from active record to orm. As I've used symfony (and doctrine) in the past. But now that I have had time to think about it, I think even -I- don't know exactly what im asking lol... beat head against desk and run out the door typing kind of day. – StrikeForceZero Oct 14 '15 at 02:38
  • Are you asking how to get the entity repository from controller? – Hari K T Oct 14 '15 at 04:32
  • 1
    create a service called 'EntityManagerProvider' with a method called 'createManager' and call it where ever you need an entityManager. – some_groceries Oct 14 '15 at 07:00
  • thanks @mojo you got me closer. – StrikeForceZero Oct 14 '15 at 18:36
  • @tftd I've updated my question with code examples showing what I'm trying to accomplish. I appreciate the help so far. – StrikeForceZero Oct 14 '15 at 18:38
  • @StrikeForceZero I saw your code but I'm really lost at what you're trying to do. First of all, if you already have an `Entity`, why do you have to specify an `EntityRepository` as a service? You can just request it via `$this->getDoctrine()->getRepository('App\MyBundle\Entity\EntityName', 'connection_name_i.e._asterisk')` ([docs](http://symfony.com/doc/current/cookbook/doctrine/multiple_entity_managers.html)). In my opinion you are waaaayyy over complicating your code. From the looks of it I'm left under the impression you are trying to "abstract" some queries, is that right? – tftd Oct 14 '15 at 21:41
  • Also, an `Entity` is an object representation of your table. It shouldn't know anything about the `repository` or whatsoever. and it should only contain properties containing your table columns (and maybe some private methods to do some really specific per entity tasks i.e generate a new user password salt when a user sets a new password). That's why you have a `repository` - to get the data for the `Entity`. If you've ever worked with [Hibernate](http://hibernate.org/), doctrine and symfony pretty much are using the same ideology and principles. – tftd Oct 14 '15 at 21:43
  • @tftd I'm working with entities whose schema cannot be modified. So I've created sperate non-entity classes that call upon these repositories. Yes I'm trying to abstract my queries. But I'm trying to use them statically inside helper classes without passing around a service object. Agent and Extension are both wrapper classes and aren't doctrine entities. They depend on the entities device, cdr, featurecode, queueconfig... Etc. – StrikeForceZero Oct 15 '15 at 01:10
  • thanks for mentioning me in your edit. how would this not follow symfony design patterns? can you explain why? i can look into an alternative if it violates the rules. services are generic objects, used globally througout the application. this fits the bill. – some_groceries Oct 16 '15 at 07:28
  • @mojo sorry, after looking at the way I worded that it was targeting you. I didn't mean to. I was trying to say that I thought what I was doing wasn't good design. What you suggested fit symfony's design pattern perfectly, I just tweaked it into a beast. I edited my question to clarify. But thank you, you defiantly put me in the right direction or sparked the thought process to get closer to my goal. – StrikeForceZero Oct 16 '15 at 17:36

0 Answers0