Databázové resource pro moduly, použitelné na více místech

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
Lebus
Člen | 3
+
0
-

Zdravím,
už mi to trvá fakt dlouho, tak se zkusím zeptat tady.
Moje aplikace poběží na jedné doméně, kde se bude moct přihlašovat neomezený počet uživatelů. Každý přihlášený uživatel má vlastní databázi a abych nemusel pro každého uživatele vytvářet nové připojení do DB, tak sem se rozhodl jít cestou jednoho připojení. A nyní se dostávám k jádru pudla. Vytvořil jsem si základní Resource třídu:

<?php
namespace App\Packages\Resource;

use Nette\Object;
use Nette\Security\User;

class DatabaseResource extends Object {

	const DATABASE_TABLE_PATTERN = '[%s.%s]';

    const PROJECT_DATABASE_PATTERN = 'db_prefix_%s';

    const CMS_DATABASE = 'cms_database';

    /**
     * @var User $user
     */
    protected $user;

    /**
     * @var \DibiConnection $database
     */
    protected $database;

    /**
     * @param \DibiConnection $database
     * @param User $user
     */
    public function __construct(\DibiConnection $database, User $user) {
        $this->database = $database;
        $this->user = $user;
    }

    /**
     * @param $table
     * @return string
     */
    protected function fromCms($table) {
        return $this->from(self::CMS_DATABASE, $table);
    }

    /**
     * @param $table
     * @return string
     */
    protected function fromProject($table) {
        if (is_null($this->projectDatabase)) {
            /** @var Identity $identity*/
            $identity = $this->user->getIdentity();

            $project = $identity->getProject();
            if (!$project) {
                throw new \InvalidArgumentException('Could not resolved project from identity.');
            }

            $this->projectDatabase = sprintf(self::PROJECT_DATABASE_PATTERN, $project);
        }

        return $this->from($this->projectDatabase, $table);
    }

    /**
     * @param $database
     * @param $table
     * @return string
     */
    protected function from($database, $table) {
        return sprintf(self::DATABASE_TABLE_PATTERN, $database, $table);
    }
}

Od této třídy dědí moje moduly, například modul Osoby viz:

<?php
namespace App\Packages\Resource;

class PersonsResource extends DatabaseResource {

    /**
     * @return \DibiConnection
     */
    public function createResource() {
        return $this->database->select('*')
            ->from($this->fromProject('persons'))->as('p');
    }

    /**
     * @return \DibiConnection
     */
    public function getTopResource() {
        $resource = $this->createResource();
        $resource->orderBy('[p.isTopTen]')->desc();
        $resource->orderBy('[p.priority]')->asc();
        return $resource;
    }
}

Všimněte si použití ->from($this->fromProject('persons')) (proto potřebuji to dědění od té základní třídy, jelikož metoda fromProject mi vrátí správnou databázi dle identity přihlášeného uživatele).
Potud je vše ještě OK, až na to, že si každý resource musím definovat v config.neon, ale to je to nejmenší co mě nyní trápí. Co mě trápí je to, že mám další resource AccountResource a tento resource bych chtěl použít pro Authenticator, ale při použití mi to napíše:

Nette\InvalidStateException
Circular reference detected for services: application.5, security.user, authenticator, authResource.

Což mě už vytáčí a nevím co s tím :-D. Možná by bylo nejlepší mít nějakou factory ResourceFactory a nejlépe, kdyby přes tuhle ResourceFactory šli vkládat závislosti v config.neon například něco takového:

services:
	router: App\RouterFactory::createRouter
	resourceFactory: [CLASS_TO_RESOURCE_FACTORY](@dibi.connection, @user)
	authenticator: App\Packages\DatabaseAuthenticator(@resourceFactory::get('accountResource'))

A pak bych si mohl do presenteru vždy zavolat pouze ResourceFactory a z tý bych si vytáhl resource, který potřebuji (PersonsResource, CompaniesResource …), ale ani tato factory mi nejde udělat, jelikož to končí na stejné chybě **Circular reference detected … **

Snad sem to popsal dost srozumitelně, za každý tip nebo navedení na správnou cestu děkuju :-)

pata.kusik111
Člen | 78
+
0
-

application.5 je s největší pravděpodobností tvoje AccountResource. Ta potřebuje ke svému vytvoření User, protože dědí od DatabaseResource a ta ve svém konstructoru injectuje User. User ke svému vytvoření potřebuje IAuthenticator (viz api pro 2.3.7). Tak se kontejner snaží vytvořit instanci IAuthenticator. A jak píšeš, tvoje implementace vyžaduje AccountResource. To je jak se dostáváš k té kruhové závislosti.

A jak tedy na to?
Nejlepší řešení je tuhletu kruhovou závislot nějak rozbít. Jiný design, který ji nevyžaduje, zvážit, jestli jsou všechny závislosti opravdu potřeba.

Jiné řešení je mít nějakou ještě úplně jinou třídu, která tuhletu skupinu tříd vytvoří.
Pak některé závislosti nemusí být přímo v konstruktoru, protože můžeš zajistit, že je tam injectneš až po tom, co se objekt vytvoří, ale ještě před tím, než ho dostane do rukou kdokoliv jiný.

Lebus
Člen | 3
+
0
-

Tohle jsem nakonec tedy vyřešil tím, že jsem si DatabaseResource ještě rozdělil nyní DatabaseResource dědí od BaseResource, který dostává pouze DibiConnection a u AccountResource dědí pouze od toho BaseResource

viz:

<?php
namespace App\Packages\Resource;

use Nette\Object;

class BaseResource extends Object {

    const DATABASE_TABLE_PATTERN = '[%s.%s]';
    const CMS_DATABASE = 'cms_database';

    /**
     * @var \DibiConnection $database
     */
    protected $database;

    /**
     * @param \DibiConnection $database
     */
    public function __construct(\DibiConnection $database) {
        $this->database = $database;
    }

    /**
     * @param $table
     * @return string
     */
    protected function fromCms($table) {
        return $this->from(self::CMS_DATABASE, $table);
    }

    /**
     * @param $database
     * @param $table
     * @return string
     */
    protected function from($database, $table) {
        return sprintf(self::DATABASE_TABLE_PATTERN, $database, $table);
    }
}

a nový DatabaseResource:

<?php
namespace App\Packages\Resource;

use Nette\Object;
use Nette\Security\User;

class DatabaseResource extends BaseResource {

    const PROJECT_DATABASE_PATTERN = 'db_prefix_%s';

    /**
     * @var User $user
     */
    protected $user;

    protected $projectDatabase = NULL;

    /**
     * @param \DibiConnection $database
     * @param User $user
     */
    public function __construct(\DibiConnection $database, User $user) {
        parent::__construct($database);
        $this->user = $user;
    }

    /**
     * @param $table
     * @return string
     */
    protected function fromProject($table) {
        if (is_null($this->projectDatabase)) {
            /** @var Identity $identity*/
            $identity = $this->user->getIdentity();

            $project = $identity->getProject();
            if (!$project) {
                throw new \InvalidArgumentException('Could not resolved project from identity.');
            }

            $this->projectDatabase = sprintf(self::PROJECT_DATABASE_PATTERN, $project);
        }

        return $this->from($this->projectDatabase, $table);
    }
}

Což funguje a to je super! :-)

Ale mám ještě malej dotaz. Tímto způsobem mi vznikne řada resourců (Persons, Account, Companies, Tasks, …) a všechny musím zapsat do config.neon, nešlo by to nějak přepsat do factory? Zkoušel jsem to, ale to mi pak vyhodí starou známou Circular reference detected…

jiri.pudil
Nette Blogger | 1029
+
+1
-

Mohl by sis teoreticky napsat rozšíření compileru, které ti ty služby nějak dynamicky najde a zaregistruje, ale připadá mi to jako kanón na vrabce – zas tolik jich snad nemáš, abys je v configu nevypsal; navíc s pluginem jde všechno snáz :)