Oznámení
LeanMapper – MultiConnection
před 5 lety
- VojtaSim
- Člen | 55
Zdravím,
nedávno jsem hledal
řešení na otázku jak se připojit k 2 databázím v LeanMappru,
z níž přihlašovací údaje k 1 získám podle přihlášeného
uživatele.
Zde je mé řešení. Funkční, nedopilované (každé připojení má svůj
Debug panel), a hlavně je to velký zásah do LeanMapperu.
Začíná to v configu, kde se nastaví dvě připojení:
defaultConnection:
class: AdminModule\Model\Connections\DefaultConnection( %database.default%, Default )
setup:
- registerFilter('sort', [@sorter, 'sort'], 'p')
backendConnection:
class: AdminModule\Model\Connections\BackendConnection( @defaultConnection::getBackendDatabaseCredentials(@user::getBackendId()), Backend )
setup:
- registerFilter('sort', [@sorter, 'sort'], 'p')
- registerFilter('priority', [@sorter, 'priority'], 'p')
- registerFilter('translateFromEntity', [@translator, 'translateFromEntity'], 'ep')
- registerFilter('translate', [@translator, 'translate'])
- AdminModule\Model\Mapper
- LeanMapper\DefaultEntityFactory
// @user::getBackendId() je metoda ve vlastní implementaci třídy User a je to zkratka pro $user->identity->backend_id
Metoda getBackendDatabaseCredentials
v defaultConnection vrací pole s přihlašovácími údaji,
které jsou uložené v tabulce, v databázi dostupné před
defaultConnection
Třídy DefaultConnection
a BackendConenction
jsou
obyčejné třídy rozšiřující třídu Connection
aby byla
zachována podpora filtrů.
Takovýchto připojení může být několik, ale musí mít unikátní
jméno + 'Connection'
a 1 připojení (výchozí) se musí
jmenovat podle názvu nastaveného v mappru. Toto připojení dostane
repositář vždy, pokud nějaké připojení neexistuje, nebo se pomocí
anotace nad repositářem neuvede jinak.
/**
* @connection backend -- backend = připojení registrované jako služba `backendConnection`
* @table user
* @entity User
*/
Tak samo se musí uvést i u Entity!
Takhle by jsme dostali chtěné připojení do repositáře, ale problém
nastává, když se z entity User
odkážu třeba na entitu
Group
, jejíž tabulka leží v jiné databázi. Tohle se řeší
pomocí customFlag
u definice @property
/**
* Class User
* @package Model\Entity
*
* @property-read int $id
* @property-read int $group_id
*
* @property Group $group m:hasOne m:connection(default) -- opět se používá jméno pod kterým je připojení registrované v configu
*/
Pokud se odkazuji na stejnou databázi není třeba
m:connection
uvádět
Technická stránka
Mapper zde zastává roli toho, co přiděluje připojení. Od toho jsem přidal pár nových metod:
/** @var Connection[] */
protected $connections;
/** @var string */
protected $defaultConnectionName = 'default';
public function __construct(Container $container) // Container je potřeba, aby byl přístup ke všem službám a tedy i připojením
{
$this->container = $container;
}
public function getConnection($name)
{
if (!isset($this->connections[$name])) {
try {
$connectionName = $name . 'Connection';
return $this->connections[$name] = $this->container->getService($connectionName);
} catch (MissingServiceException $e) {
if ($name === $this->defaultConnectionName) {
throw new InvalidStateException("Default connection with name '$name' does not exist!");
}
return $this->getConnection($this->defaultConnectionName);
}
}
return $this->connections[$name];
}
Dále jsem upravil i Repository
:
/** @var string */
protected $connectionName;
public function getConnection()
{
return $this->connection ?: $this->connection = $this->mapper->getConnection($this->getConnectionName());
}
protected function getConnectionName()
{
if ($this->connectionName === null) {
$connectionName = AnnotationsParser::parseSimpleAnnotationValue('connection', $this->getDocComment());
if ($connectionName !== null) {
return $this->connectionName = $connectionName;
}
}
return $this->connectionName;
}
Místo ->connection
se musí používat
->getConnection()
, aby se vyhnulo cirkulárních referencím,
když by se Connection předávalo __construct()
. Implementace
přes getter by byla možná, ale:
Funkční, nedopilované…
Hodně změn jsem musel udělat v Entity
, jednak pro podporu
m:connection
a jednak, abych mohl předat název připojení z
Entity
až do Result
, který je v Entity
obalený.
V metodách, které získávají hodnoty z různých relací a kontrolují změny…
getHasManyRowDifferences, addToOrRemoveFrom, getBelongsToManyValue, getBelongsToOneValue, getHasManyValue, getHasOneValue, markAsUpdated
…jsem přidal:
$targetConnection = $property->hasCustomFlag('connection') ? $property->getCustomFlagValue('connection') : $this->connectionName;
// pokud není nastaveno m:connection použije se connection podle této entity
Tato hodnota se pak předává před Row
do Result
do metod:
getReferencedRow, getReferencingRows, addToReferencing, removeFromReferencing, createReferencingDataDifference
a v těchto metodách se už následně pomocí Mappru vybere dané připojení a použije se pro select.
Known bugs
Pokud se použijí vazební tabulky tak tabulka musí být ve stejné databázi jako cílová tabulka, ovšem cesta zpět je horší, protože by vazební tabulka by taky musela ležet v cílené databázi.
Možné řešení: Samostatná entita pro vazební tabulku | jiný způsob než m:connection?
Jeden příklad
Jak se přihlásit do databáze pomoci uživatele, který ještě není
přihlášený? K***a těžko…
Někdo tu může uznat jako prasárnu, ale jak jinak?
// backendConnection
class BackendConnection extends Connection
{
private $connectionName;
public function __construct($config, $name)
{
if (!is_null($config)) {
parent::__construct($config, $name);
}
$this->connectionName = $name;
}
public function reconnect($config)
{
$this->__construct($config, $this->connectionName);
}
}
// Authenticator
/** @var \SystemContainer|\Nette\DI\Container */
private $container;
public function __construct(BackendRepository $backendRepository, Nette\DI\Container $container)
{
$this->backendRepository = $backendRepository;
$this->container = $container;
}
public function authenticate(array $credentials)
{
list($email, $password) = $credentials;
try {
$loginAccount = $this->backendRepository->getLoginByEmail($email);
if ($loginAccount->password == $this->backendRepository->hashPassword($password, $password)) {
$config = array('driver' => 'mysql', 'profiler' => 'true',
'host' => $loginAccount->databaseHost,
'username' => $loginAccount->databaseUser,
'password' => $loginAccount->databasePassword,
'database' => $loginAccount->databaseName,
);
$connection = $this->container->backendConnection;
$connection->reconnect($config);
$accountRepository = $this->container->accounts;
$account = $accountRepository->get($loginAccount->id);
return new Nette\Security\Identity($account->id, array(), $account->getRowData());
}
throw new Nette\Security\AuthenticationException("Entered email address or password is incorrect!", self::INVALID_CREDENTIAL);
} catch (Exceptio $e) {
throw new Nette\Security\AuthenticationException($e->getMessage(), self::IDENTITY_NOT_FOUND);
}
}