Symfony - Wrapper/Helper classes having access to

2019-08-11 05:21发布

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.

0条回答
登录 后发表回答