Nettrine / Doctrine – Doctrine DBAL 4.x + ORM 3.x

Felix
Nette Core | 1256
+
+8
-

Nettrine balicky prosly tento mesic velkou aktualizaci a pridaly spoustu novych funkci. Osobne z toho mam velmi dobry pocit.

Features

  1. Podpora aktualni Doctrine DBAL 4.x.
  2. Podpora aktualni Doctrine ORM 3.x.
  3. Podpora pro vice databazi.
  4. Podpora pro Doctrine middlewares.

Deprecations

  1. Odstranena podpora pro anotace.
  2. Odstranena podpora pro doctrine/cache.

Doctrine DBAL 4.x + ORM 3.x

Co noveho prinasi Doctrine se muzete docist primo na oficialnich strankach:

Existuji i upgrade manualy primo od Doctrine teamu:

Konfigurace

Soucasti zmen bylo i vylepseni dokumentace.

Stara konfigurace

Takto vypadala stare konfigurace, kde bylo zapotrebi pouzivat vice extensions. To jiz v nove verzi neni potreba.

extensions:
	console: Contributte\Console\DI\ConsoleExtension(%consoleMode%)

	nettrine.annotations: Nettrine\Annotations\DI\AnnotationsExtension
	nettrine.cache: Nettrine\Cache\DI\CacheExtension

	nettrine.dbal: Nettrine\DBAL\DI\DbalExtension
	nettrine.dbal.console: Nettrine\DBAL\DI\DbalConsoleExtension

	nettrine.orm: Nettrine\ORM\DI\OrmExtension
	nettrine.orm.cache: Nettrine\ORM\DI\OrmCacheExtension
	nettrine.orm.console: Nettrine\ORM\DI\OrmConsoleExtension(%consoleMode%)
	nettrine.orm.annotations: Nettrine\ORM\DI\OrmAnnotationsExtension

nettrine.dbal:
	connection:
		driver: %database.driver%
		host: %database.host%
		port: %database.port%
		dbname: %database.dbname%
		user: %database.user%
		password: %database.password%
		charset:  UTF8
		serverVersion: '15.0'
	debug:
		panel: %debugMode%
		sourcePaths: [%appDir%]

nettrine.orm:
	entityManagerDecoratorClass: App\Model\Database\EntityManagerDecorator

nettrine.orm.annotations:
	mapping:
		App\Domain\Database: %appDir%/Domain/Database
Nova konfigurace

Nova konfigurace vyzaduje pouze DBAL a ORM extension. Vse konfiguruje automaticky a zaroven je mozne nove pridat vice pripojeni do DB a taktez vice EntityManageru.

extensions:
	console: Contributte\Console\DI\ConsoleExtension(%consoleMode%)

	nettrine.dbal: Nettrine\DBAL\DI\DbalExtension(%debugMode%)
	nettrine.orm: Nettrine\ORM\DI\OrmExtension

nettrine.dbal:
	debug:
		panel: %debugMode%

	connections:
		default:
			driver: %postgres.driver%
			host: %postgres.host%
			port: %postgres.port%
			dbname: %postgres.dbname%
			user: %postgres.user%
			password: %postgres.password%
			charset:  UTF8
			serverVersion: 15.0.0

		second:
			driver: %mariadb.driver%
			host: %mariadb.host%
			port: %mariadb.port%
			dbname: %mariadb.dbname%
			user: %mariadb.user%
			password: %mariadb.password%
			charset:  UTF8
			serverVersion: 10.10.0

nettrine.orm:
	managers:
		default:
			connection: default
			mapping:
				App:
					directories: [%appDir%/Domain/Database]
					namespace: App\Domain\Database
		second:
			connection: second
			mapping:
				App:
					directories: [%appDir%/Domain/Database]
					namespace: App\Domain\Database

Balicky

Takto muzete pouzit novou verzi jeste pred finalnim vydanim.

{
 "require": {
    "php": ">=8.2",
    "contributte/console": "^0.11.0",
    "contributte/event-dispatcher": "^0.10.0",

    "nettrine/dbal": "^0.10.0",
    "nettrine/orm": "^0.10.0",
    "nettrine/extra": "^0.2.0",
    "nettrine/fixtures": "^0.8.0",
    "nettrine/migrations": "^0.10.0"
  },
  "prefer-stable": true,
  "minimum-stability": "dev"
}

Dejte vedet jak se vam s tim pracuje.

Gappa
Nette Blogger | 210
+
0
-

Migroval jsem konfiguraci podle návodu výše a zatím jsem narazil jen tohle, že se stihlo změnit :)

Většinu věcí jsem dohledal přímo ve schématu (DbalExtension, resp. OrmExtension), protože registruji vlastní types/typesMapping atp.

Co se samotného fungovaní týče, tak to na první pohled funguje 👍😁 Bude potřeba ale víc testování.

Tracy panel Queries – koukám, že zmizel source dotazu (škoda), ale přibyl sloupec s parametry (super).

Díky! :)

redwormik
Člen | 9
+
0
-

Ahoj,

podařilo se někomu s Doctrine ORM 3.x rozchodit entity s Nette\SmartObject? Od 3.0 Doctrine používá „nové“ proxy, co využívají symfony/var-exporter (https://github.com/…ostTrait.php) a ty se nekamarádí s magickými metodami ve SmartObjectu.

Mám entitu:

<?php

declare(strict_types=1);

namespace App\Model;

use Doctrine\ORM\Mapping as ORM;
use Nette;


/**
 * @property-read int $id
 * @property-read string $name
 */
#[ORM\Entity]
class Role
{
	use Nette\SmartObject;

	#[ORM\Id]
	#[ORM\Column]
	private int $id;

	#[ORM\Column]
	private string $name;


	public function __construct(int $id, string $name)
	{
		$this->id = $id;
		$this->name = $name;
	}


	public function getId(): id
	{
		return $this->id;
	}


	public function getName(): string
	{
		return $this->name;
	}
}

Entitu dostanu jako proxy (přes lazy asociaci nebo EntityManager::getReference) a chci použít magické properties:

$role = $em->getReference(App\Model::class, 1);
echo $role->id; // nepustí inicializaci, vrátí 1; tohle dokonce funguje lépe než 2.x
echo $role->name; // spadne

Jak funguje LazyGhostTrait?

  1. dostane od Doctrine lazy properties a inicializátor a properties unsetne, aby se volal jeho __get/__set
  2. pokud se zavolá __get/__set (chci proměnnou, co nevidím/neexistuje)
    1. zjistí, jestli property je lazy a jestli je (resp. byla by, kdyby nebyla unsetnutá) přístupná z místa, odkud se přistupuje (debug_backtrace magie)
    2. pokud je lazy+přístupná a objekt není inicializovaný, pustí inicializaci a simuluje přístup z předchozího scope
    3. pokud je lazy+přístupná, objekt+proměnná jsou inicializované, tak pro __get simuluje přístup z předchozího scope (tohle je IMHO edge-case, takový přístup většinou nepůjde přes __get)
    4. pokud existují původní (tady Nette\SmartObject) __get/__set, tak je zavolá
    5. jinak (ne-lazy/neexistující/nepřístupná property) simuluje přístup z předchozího scope

První problém – nespuštění inicializace / „dvojitý“ __get: LazyGhostTrait při přístupu „z venku“ nepustí inicializaci a pustí (přes Nette\SmartObject __get) metodu getName, ten v příkladu vrací $this->name. Ten ale není inicializovaný, takže getName spadne.
Aby to fungovalo, musel by se znova pustit LazyGhostTrait __get (tentokrát z privátního scopu), ale to se nestane, protože __get na name už probíhá. To se dá vyřešit – magické a skutečné properties se musí jmenovat jinak.

Druhý problém – inicializace v Doctrine / „kouzelný“ __set: Při inicializaci Doctrine nastavuje hodnoty proměnných přes ReflectionProperty. To normálně magické metody nepouští, ale pokud je property unsetnutá, tváří se jako by nebyla, takže nastavení z privátního scope i přes ReflectionProperty (kterou nezískám, pokud by proměnná normálně neexistovala) pouští __set. Doctrine nastaví objekt jako inicializovaný předtím, než nastavuje property (https://github.com/…itOfWork.php#…), takže LazyGhostTrait nenastaví property přímo (jinak přístup přes ReflectionProperty pozná), ale volá Nette\SmartObject __set. Setter tady neexistuje, proto to spadne. Navíc kvůli prvnímu problému chci mít magickou property pojmenovanou jinak, tady bych ji musel mít pojmenovanou stejně.

Editoval redwormik (31. 1. 8:53)

Felix
Nette Core | 1256
+
+1
-

@redwormik Ja uz dlouho SmartObject nepouzivam, tak nedokazu rict. Ale dnes v dobe kdy existuje v PHP 8.4 property hooks (https://www.php.net/…ty-hooks.php), tak jeste potrebujes SmartObject?

redwormik
Člen | 9
+
0
-

Tak možná s přejmenováním magických properties + trochou magie, co fixne nastavení přes Reflection:

<?php

declare(strict_types=1);

namespace App\Model;

use Nette;


trait EntitySmartObject
{
	use Nette\SmartObject {
		__set as private netteSet;
	}


	public function __set(string $name, mixed $value): void
	{
		$frame = debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS, 3)[2];
		if (
			($frame['class'] ?? null) === \ReflectionProperty::class &&
			$frame['object']->name === $name &&
			$frame['object']->class === self::class &&
			!$frame['object']->isInitialized($this) &&
			!$frame['object']->isDynamic()
		) {
			$this->$name = $value;
			return;
		}

		$this->netteSet($name, $value);
	}
}
<?php

declare(strict_types=1);

namespace App\Model;

use Doctrine\ORM\Mapping as ORM;
use Nette;


/**
 * @property-read int $Id
 * @property-read string $Name
 */
#[ORM\Entity]
class Role
{
	use EntitySmartObject;

	#[ORM\Id]
	#[ORM\Column]
	private int $id;

	#[ORM\Column]
	private string $name;


	public function __construct(int $id, string $name)
	{
		$this->id = $id;
		$this->name = $name;
	}


	public function getId(): id
	{
		return $this->id;
	}


	public function getName(): string
	{
		return $this->name;
	}
}
$role = $em->getReference(App\Model::class, 1);
echo $role->Id;
echo $role->Name;

Editoval redwormik (31. 1. 10:07)

redwormik
Člen | 9
+
0
-

@Felix Nepotřebuju, řešim to spíš pro úplnost, jestli to jde/nejde.

EDIT: případně kvůli upgradu Doctrine ve starších projektech, co SmartObject používají

BTW – property hooks ještě v Doctrine entitách nejdou, ne? Alespoň 2.20 je nepustí, nevim, jak to mají vymyšlený ve 3.x, podle https://github.com/…issues/11624 nejdou, ale výjimka (https://github.com/…m/pull/11628) ve 3.x není.

Editoval redwormik (31. 1. 10:01)

VáclavČerný
Člen | 4
+
0
-

@Felix Ahoj, díky za aktualizaci. Při vyzkoušení jsem narazil na dva drobné problémy:

1. The item 'nettrine.dbal › connections › default › password' expects to be string, dynamic given.
Pro nastavení hesla používám ENV a do konfigurace se pak dostane Nette\DI\DynamicParameter.
Bylo by řešením v extension ve schématu configu povolit dynamické parametry 'password' => Expect::string()->dynamic(), nebo lze řešit jinak?

2. Service of type Doctrine\ORM\Configuration not found., BeberleiBehaviorExtension.php:169, nettrine/extensions-beberlei
Službu není problém do DI přidat. Spíš je otázka, zda by to neměla udělat automaticky už ORM extension.

Díky

VáclavČerný
Člen | 4
+
0
-

Narazil jsem ještě na jeden další problém. Jakmile u DBAL extension nastavím resultCache, tak “cached query” spadne na Cannot serialize Symfony\Component\Cache\Adapter\AbstractAdapter, protože parametr se dostane do Doctrine\DBAL\Connection a QueryCacheProfile::generateCacheKeys se pokusí serializovat AbstractAdapter (serialize($connectionParams)).

Felix
Nette Core | 1256
+
0
-

Ahoj @VáclavČerný, zrovna vcera jsem vydal verzi s podporou dynamicke konfigurace.

https://github.com/…/tag/v0.10.1

U toho druheho problemu, zalozis mi prosim ticket a posles tam ukazkovou konfiguraci? Pridam to do testu a odhalime kde je problem.

VáclavČerný
Člen | 4
+
0
-

@Felix Super, díky. Ticket zde https://github.com/…al/issues/92.