Adaptacja zewnętrznych bibliotek

Tym razem chciałbym opisać przykład, na który natknąłem się ostatnio.

Jakiś czas temu zacząłem w ramach nauki “Clean Code” oraz architektury skupionej wokół przypadków użycia, rozwijać swój własny framework do PHP. Jest to nadal projekt we wczesnej fazie rozwoju, nie nadający się do publikacji, więc nie ma się czym specjalnie chwalić. Jako bazę pod framework wykorzystałem kontener aplikacji napisany przez mojego kolegę @bar4bor.

Projekt był już przepisywany chyba z 5 razy (różne jego części), postawiłem w końcu Jenkinsa jako serwer CI dla PHP (napiszę o tym jeszcze) i w związku z tym postanowiłem podzielić kod na sensowne paczki i zarządzać tym wszystkim z Composer’a. Jako, że @bar4bor już od dawna namawiał mnie do testów jednostkowych (wcześniej stosowałem tylko testy akceptacyjne pisane w Behat), postanowiłem dopisać testy do poszczególnych paczek.

I tu pojawił się problem.

Częścią mojego kodu były adaptery do zewnętrznych bibliotek, pozwalające na zarządzanie nimi z kontenera. I okazało się, że są napisane tak, że nie da się napisać dla nich testów jednostkowych. Postanowiłem je w związku z tym poprawić. Posłużę się tutaj jednym z tych przykładów.

Mój adapter dla “Doctrine2” w pierwotnej wersji wyglądał tak:

class DoctrineEntityManagerProvider
{
    /** @var \Saigon\Conpago\IDbConfig */
    private $dbConfig;

    /** @var \Saigon\Conpago\IDoctrineConfig */
    private $doctrineConfig;

    /** @var \Doctrine\ORM\EntityManager */
    private $entityManager;

    /** @var array */
    private $dbParams = null;

    /**
     * @param \Saigon\Conpago\IDbConfig $dbConfig
     * @param \Saigon\Conpago\IDoctrineConfig $doctrineConfig
     */
    public function __construct(IDBConfig $dbConfig, IDoctrineConfig $doctrineConfig)
    {
        $this->dbConfig = $dbConfig;
        $this->doctrineConfig = $doctrineConfig;

        $paths = array($this->doctrineConfig->getModelPath());

        $this->setDbParams();
        $config = Setup::createAnnotationMetadataConfiguration($paths, $this->doctrineConfig->getDevMode());
        $this->entityManager = EntityManager::create($this->dbParams, $config);
    }

    private function setDbParams()
    {
        $this->dbParams = array(
            'driver' => $this->dbConfig->getDriver(),
            'user' => $this->dbConfig->getUser(),
            'password' => $this->dbConfig->getPassword(),
            'dbname' => $this->dbConfig->getDbName()
        );
    }

    /**
     * @return \Doctrine\ORM\EntityManager
     */
    public function getEntityManager()
    {
        return $this->entityManager;
    }
}

W takiej implementacji nie ma możliwości napisania testów, a także po analizie stwierdziłem, że złamana jest zasada SRP (klasa zarówno produkuje jak i dostarcza instancję EntityManager’a).

Rozwiązanie tego problemu po przemyśleniu, okazało się banalne. Pierwsze co należało zrobić, to poprawić kod tak, aby nie łamał zasady SRP. Powstały 2 klasy:

EntityManagerFactory:

class EntityManagerFactory
{
    private $config;
    /**
     * @var IDbConfig
     */
    private $dbConfig;

    /**
     * @var IDoctrineConfig
     */
    private $doctrineConfig;

    /**
     * @var array
     */
    private $dbParams = null;

    /**
     * @param IDbConfig $dbConfig
     * @param IDoctrineConfig $doctrineConfig
    */
    public function __construct(IDBConfig $dbConfig, IDoctrineConfig $doctrineConfig)
    {
        $this->dbConfig = $dbConfig;
        $this->doctrineConfig = $doctrineConfig;

        $paths = array($this->doctrineConfig->getModelPath());

        $this->setDbParams();
        $this->config = Setup::createAnnotationMetadataConfiguration($paths, $this->doctrineConfig->getDevMode());
    }

    private function setDbParams()
    {
        $this->dbParams = array(
            'driver' => $this->dbConfig->getDriver(),
            'user' => $this->dbConfig->getUser(),
            'password' => $this->dbConfig->getPassword(),
            'dbname' => $this->dbConfig->getDbName()
        );
    }

    /**
     * @return EntityManagerInterface
     */
    public function createEntityManager()
    {
        return EntityManager::create($this->dbParams, $this->config);
    }
}

oraz

DoctrineDao

abstract class DoctrineDao
{
    /**
     * @var IDoctrineConfig
     */
    private $doctrineConfig;

    /**
     * @var EntityManagerInterface
     */
    private $entityManager;

    /**
     * @param IDoctrineConfig $doctrineConfig
     * @param EntityManagerInterface $entityManager
     */
    public function __construct(IDoctrineConfig $doctrineConfig, EntityManagerInterface $entityManager)
    {
        $this->entityManager = $entityManager;
        $this->doctrineConfig = $doctrineConfig;
    }

    /**
     * @param $shortClassName
     *
     * @return string
     */
    protected function getModelClassName($shortClassName)
    {
        return $this->doctrineConfig->getModelNamespace() . "\\" . $shortClassName;
    }

    /**
     * @return \Doctrine\ORM\EntityManager
     */
    protected function getEntityManager()
    {
        return $this->entityManager;
    }
}

To jednocześnie rozwiązało drugi problem. Testów.

Przy tak napisanych klasach możemy przetestować zarówno fabrykę (sprawdzić czy zwraca obiekt zgodny z oczekiwaniami) oraz Dao (poprzez przekazanie mu TestDouble’a EntityManagerInterface).

Pozostaje już tylko odpowiednia rejestracja w kontenerze tak, aby fabryka była używana do rozwiązywania zależności EntityManagerInterface i całość pięknie działa.

W kontenerze, którego używam wygląda to tak:

class DoctrineDatabaseModule implements IModule
{
    public function build(IContainerBuilder $builder)
    {
        $builder
            ->registerType('Saigon\Conpago\Database\Doctrine\EntityManagerFactory');

        $builder
            ->register(function (IContainer $c)
            {
                /** @var EntityManagerFactory $entityManagerFactory */
                $entityManagerFactory = $c->resolve('Saigon\Conpago\Database\Doctrine\EntityManagerFactory');

                return $entityManagerFactory->createEntityManager();
            })
            ->asA('Doctrine\ORM\EntityManagerInterface');
    }
}

Comments:0

Leave a Reply

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