Nextras\Orm – dalsi orm, fork YetOrmu

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

Toto je jednodenni pokus, pracuje na novem orm, ale zatim neni hotove. Kdy bude nevim. Toto opravdu nepouzivejte.

Ma cenu vubec obhajovat proc dalsi ORM? YetORM se mi moc libi, ale v nekterych aspektech se mi vubec nesedlo. Proto Nextras\Orm.

https://github.com/nextras/orm

Nejake hlavni myslenky a vlastnosti:

  • kompatibilní s Nette 2.1-dev
  • Entity mohou byt vytvoreny bez db
  • Potomek Entity nema pristup k ActiveRow
  • Entita se persistuje, zadne update ci create
  • mame dao a facade
  • getery a setery jsou protected/private
  • vsechny property jsou cachovane (funguje spravne i cache property zavisle na jine property!)

Nejlepe napovi asi testy v nette testeru. Na toto chovani jsem pysny :)

EDIT:

  • zacal jsem dneska odpo, takze …
  • netestovane praxi …
  • WIP …
  • zadne skarede komentare! :)
  • jo, ndab konci

Editoval hrach (1. 2. 2014 18:18)

castamir
Člen | 629
+
0
-

@hrach asi budu zlý (sry :D), ale musím se zeptat na mou již tradiční otázku: jak bys realizoval speciální selecty (auto join do jiné tabulky) nebo další automatické inserty do jiných tabulek při uložení nové entity (např. při closure table)?

Jinak to vypadá samozřejmě pěkně. Je to zas velký progress YetORM, takže gj ;)

Editoval castamir (27. 4. 2013 22:18)

hrach
Člen | 1844
+
0
-

speciální selecty (auto join do jiné tabulky)

netusim co by to melo znamenat :D

další automatické inserty do jiných tabulek při uložení nové entity

onPersist by nestacilo?

castamir
Člen | 629
+
0
-

@hrach http://blog.profitak.cz/?p=5

přidání podobných joinů do selectu

SELECT c.*, cc2.ancestor, cc.descendant, cc.depth
FROM category c
JOIN category_closure cc ON (c.category_id = cc.descendant)
LEFT JOIN category_closure cc2 ON (cc2.descendant = cc.descendant AND cc2.depth = 1)
WHERE cc.ancestor = 8 AND cc.depth > 0

aneb něco, co pokud si dobře pamatuju tak NDB ještě neumí (aliasovat). Lze to obejít přes query, ale tím nedostaneš ActiveRow…

Editoval castamir (27. 4. 2013 22:27)

Šaman
Člen | 2668
+
0
-

Pěkné, určitě vyzkouším. Ale zůstává jeden problém, který mi taky trochu vadil už u YetORMu a asi je spjatý s NDb. Mohu vytvořit kolekci jinak, než jako Ndb Selection?
Konkrétně v tomto případě bych vůbec nechtěl vytvářet entitu Tag a pracovat s tagy jako s objekty. Tag je vlastnost entity Book a rád bych napsal getTags() tak, aby mi vracela pole stringů. (Což myslím nejde napsat ze směru od $book->cosi, ale pomocí query). Ale s výsledkem bych rád pracoval stejně jako s klasickou kolekcí (typicky na ni budu aplikovat order by). Dá se tohle nějak pořešit?

//Edit: Koukám, že mezitím popsal podobný problém @castamir. Prostě tím, že to funguje nad Ndb, tak musím entity a kolekce tvořit pomocí ActiveRow a Selection. Asi jsem si jen zatím nezvykl na tuto filosofii a trochu se bojím toho, že co nebudu umět zapsat, tam mi nepomůže ani čistá SQL query, kde by to napsat šlo..
Výhoda je naprostá jednoduchost a transparentnost těchto ORMů, protože ActiveRow za nás vyřeší většinu práce s generováním dotazů.

Editoval Šaman (27. 4. 2013 22:36)

enumag
Člen | 2118
+
0
-

vsechny property jsou cachovane (funguje spravne i cache property zavysle na jine property!)

Promiň, ale tahle hrubka fakt bije do ksichtu. ;-)

Jinak moc hezké, přesně řešíš ty problémy, kvůli kterým jsem s YetORM váhal (hlavně persist) a chtěl jsem ho forknout.

@castamir tu už psal, že ORM tu poslední dobou rostou jako houby po dešti. Ještěže jsem doteď neměl čas napsat si vlastní! Teď mám mnoho možností z čeho vybírat. :-D

castamir
Člen | 629
+
0
-

@enumag +1

Mám asi týden na to dokončit ten svůj (chci ho použít v novém projektu). Dělám si k němu i generátor (já vím, jsem nehorázně zhnilej psát ty kostry entit ručně :D )

hrach
Člen | 1844
+
0
-

Pocitam s podporou pro to, aby to nebylo nad nette database, respektive s volenejsim api :)

Šaman
Člen | 2668
+
0
-

Jestli se ti to podaří, tak snad konečně začnu s klidným svědomím používat ORM framework :)
V Doctrině2 jsem válčil s jejich DQL, v Petrovo ORMu s neexistující dokumentací, Fabikovo DAO bylo docela fajn, ale narazil jsem na pár problémů, které jsem vyřešil migrací k YetORM. S tím jsem zatím nejvíc spokojený, takže jestli odstraníš i jeho slabiny, tak bude důvod k oslavě.

Tharos
Člen | 1030
+
0
-

@hrach: Mám první dotaz na tohle „naprosto brilantní ORMko“ :). Jak bys v tomhle návrhu řešil například přidání/odebrání tagu knihy? Myslím včetně persistence.

Neber to prosím jako nějakou provokaci, ale jsem upřímně zvědav, jak na tenhle návrh budeš nahlížet poté, co s ním strávíš více, než jedno odpoledne. :–P Já jsem pracoval s původním YetORMem ve dvou reálných projektech a narazil jsem hned na několik problémů, které mi přišly tak nějak principiální… No, jsem upřímně zvědav, jak dopadne tenhle pokus. :)

Editoval Tharos (28. 4. 2013 0:19)

hrach
Člen | 1844
+
0
-

@tharos a i ostatni: zde je nejaky road plan:

  • validace pred persistenci
  • zbaveni se zavislosti na nette\database\table, pridani vrstvy pro mapovani, proste nejakeho mapperu, ale bude to velice easy :)
  • parser pro use statement (idealne nekde vygrabovat)
  • pridani podpory pro napovidani entity pro IDE
/**
 * @property Book[]|EntityCollection $books
 */
class Author extends Entity {}
  • automaticka sprava vazev, 1:N, M:N. pribudou dalsi tridy *collection, ktere budou automaticky implementovat spravu techto vazeb. nebude tak treba tento boilerplate. Api by mohlo vypadat treba takto
/**
 * @property HasOneCollection $author (author_id, book)
 */
class Book extends Entity {}
/**
 * @property HasManyCollection $books (book, author_id)
 * @property HasManyCollection $translatedBooks (book, translator_id)
 */
class Author extends Entity {}

Kolekce samozrejme budou mit patricne api a metody pro jejich persistovani. Ano, je mi jasne, ze narazim na plno problemu, ale tak nejak si to vysnivam :)

Editoval hrach (28. 4. 2013 0:33)

castamir
Člen | 629
+
0
-

používám pár další rozšíření properties, takže pro inspiraci

/**
 * @property enum $pole {allowed x,z,y}
 * @property set $mnozina {allowed x,z,y}
 * @property string|NULL $prazdny
 * @property string $defaultni {default ahoj}
 * @property string $dlouhyNazev {column dlouhy_nazev}
 */

Datový typ budu mít i konkrétní instance Entit nebo obecně datové typy PHP nebo databáze (poradím si s oběma)

/**
 * @property string $title
 * @property Author $author
 */
class Book {}

defaultní vazba je samozřejmě 1:1, ale 1:M a N:M chci také doplnit, ale sám ještě nemám vymyšlené jak ;)

Editoval castamir (28. 4. 2013 0:43)

Tharos
Člen | 1030
+
0
-

hrach napsal(a):

  • parser pro use statement (idealne nekde vygrabovat)

Kdyby Tě zajímal ten z „mého ORMka“, tak tady ho máš:

<?php

namespace Tharos\Reflection;

/**
 * @author Vojtěch Kohout
 */
class Aliases
{

	/** @var array */
	private $aliases = array();

	/** @var string */
	private $current = '';

	/** @var string */
	private $lastPart = '';


	public function resetCurrent()
	{
		$this->current = $this->lastPart = '';
	}

	/**
	 * @param string $name
	 */
	public function appendToCurrent($name)
	{
		if ($this->current !== '') {
			$this->current .= '\\';
		}
		$this->current .= $this->lastPart = $name;
	}

	/**
	 * @param string $name
	 */
	public function setLast($name)
	{
		$this->lastPart = $name;
	}

	public function finishCurrent()
	{
		$this->aliases[$this->lastPart] = $this->current;
		$this->resetCurrent();
	}

	/**
	 * @return array
	 */
	public function getAll()
	{
		return $this->aliases;
	}

}
<?php

namespace Tharos\Reflection;

use Tharos\Exception\UtilityClassException;

/**
 * @author Vojtěch Kohout
 */
class AliasesParser
{

	const STATE_WAITING_FOR_USE = 1;

	const STATE_GATHERING = 2;

	const STATE_IN_AS_PART = 3;

	const STATE_JUST_FINISHED = 4;


	/**
	 * @throws UtilityClassException
	 */
	public function __construct()
	{
		throw new UtilityClassException('Cannot instantiate utility class ' . get_called_class() . '.');
	}

	/**
	 * @param string $source
	 * @return array
	 */
	public static function parseSource($source)
	{
		$aliases = new Aliases;

		$states = array(
			self::STATE_WAITING_FOR_USE => function ($token) use ($aliases) {
				if (is_array($token) and $token[0] === T_USE) {
					$aliases->resetCurrent();
					return AliasesParser::STATE_GATHERING;
				}
				return AliasesParser::STATE_WAITING_FOR_USE;
			},
			self::STATE_GATHERING => function ($token) use ($aliases) {
				if (is_array($token)) {
					if ($token[0] === T_STRING) {
						$aliases->appendToCurrent($token[1]);
					} elseif ($token[0] === T_AS) {
						return AliasesParser::STATE_IN_AS_PART;
					}
				} else {
					if ($token === ';') {
						$aliases->finishCurrent();
						return AliasesParser::STATE_WAITING_FOR_USE;
					} elseif ($token === ',') {
						$aliases->finishCurrent();
					}
				}
				return AliasesParser::STATE_GATHERING;
			},
			self::STATE_IN_AS_PART => function ($token) use ($aliases) {
				if (is_array($token)) {
					if ($token[0] === T_STRING) {
						$aliases->setLast($token[1]);
						$aliases->finishCurrent();
						return AliasesParser::STATE_JUST_FINISHED;
					}
				}
				return AliasesParser::STATE_IN_AS_PART;
			},
			self::STATE_JUST_FINISHED => function ($token) use ($aliases) {
				if ($token === ';') {
					return AliasesParser::STATE_WAITING_FOR_USE;
				}
				return AliasesParser::STATE_GATHERING;
			}
		);

		$state = $states[self::STATE_WAITING_FOR_USE];
		foreach (token_get_all($source) as $token) {
			$state = $states[$state($token)];
			if (is_array($token)) {
				$token[3] = token_name($token[0]);
			}
		}

		return $aliases->getAll();
	}

}

Je to takovej callback-based stavovej automat. :) Měl by pokrývat vše myslitelné (včetně as, aliasů, více hodnot oddělených čárkou)…

  • automaticka sprava vazev, 1:N, M:N

Já teď u sebe zapisuji vazby v kostce takhle:

/**
 * @property Tag[] $tags m:hasMany
 * @property Author|null $author m:hasOne
 * @property Author|null $maintainer m:hasOne(maintainer_id)
 * @property Something[] $somethings m:belongsToMany(book_id:something)
 */

Naimplementoval jsem si i trochu neobvyklé vazby belongsToMany a belongsToOne, přičemž v závorce se u všech vazeb dají vždy ještě upřesnit vazební sloupce a tabulky. No, zatím to obsáhlo vše, co jsem potřeboval. Super je, že u většiny entit se pak dá vše zapsat jen pomocí anotací.

K tomuhle už bohužel kód takhle snadno nepošlu, protože logika celého toho mapování je spletitější…

Určitě parsuj (jakýmkoliv způsobem) use statement, protože pak se ty anotace úžasně pročistí.

No, přeji příjemnou zábavu. :)

Editoval Tharos (28. 4. 2013 10:08)

hrach
Člen | 1844
+
0
-

@Tharos: nechces poslat pull? (mergnutím se občas omylem aktivují i push práva;)

hrach
Člen | 1844
+
0
-

ad use – jeste by bylo fajn ten tvuj porovnat (vykon :D) s tim, kterej tu kdysi nekdo odkazoval ze je v doctrine, nebo kde…

Tharos
Člen | 1030
+
0
-

To by určitě bylo fajn, samotného by mě zajímal výsledek. :) Přiznám se, že jsem moc velkou rešerši po okolí nedělal.

Pull tedy ještě připravovat nebudu. Ono by byl stejně jednoduchý – přidat tyhle dvě třídy je to nejmenší, ale jejich použití už by stejně bylo na Tobě. Ten můj parser prostě vrátí pole ve tvaru například ['Tag' => 'Model\Entity\Tag', 'Http' => 'Nette\Http'] atp. Pak je stejně ještě zapotřebí u každé anotace reálně rozhodnout, jakého je typu (zda typ začíná na \, zda je dostupný relevantní alias, jaký je namespace celého souboru…). No a to už si Ty umístíš určitě nejlíp.

Ad motivace) Přispěl bych rád, ale problémem je, že jsem před pár dny právě dopsal první verzi podobného ORM nad Dibi a zatím je to přesně to, co jsem si vždycky přál (troufám si tvrdit, že aktuálně je i vyspělejší – už umí třeba i to mapování vazeb skrze anotace)… Plus si trochu nejsem jist ve věci zde nastíněné persistence – to jsem právě zvědav, jak to dopadne ;). Viz třeba právě správa a persistence M:N vazeb.

V každém případě držím palce, protože i když tohle ORM asi přímo nevyužiji, jeho sledování bude pro mě minimálně inspirativní.

Editoval Tharos (21. 6. 2013 14:17)

Filip Procházka
Moderator | 4668
+
0
-

@Tharos ten pull prosím udělej :) Už dlouho si na to brousím zuby (klasicky není čas …) a když to máš už hotové, tak jedině dobře!

Jan Tvrdík
Nette guru | 2595
+
0
-

hrach wrote:

  • automaticka sprava vazev, 1:N, M:N. pribudou dalsi tridy *collection, ktere budou automaticky implementovat spravu techto vazeb. nebude tak treba tento boilerplate. Api by mohlo vypadat treba takto
/**
 * @property HasOneCollection $author (author_id, book)
 */
class Book extends Entity {}
/**
 * @property HasManyCollection $books (book, author_id)
 * @property HasManyCollection $translatedBooks (book, translator_id)
 */
class Author extends Entity {}

Víc se mi líbí zápis, který používá Petrovo ORM, tj.

use Nextras\Orm;

/**
 * @property Author $author {m:1 author_id, book}
 */
class Book extends Orm\Entity {}

/**
 * @property Orm\OneToMany $books {1:m book, author_id}
 * @property Orm\OneToMany $translatedBooks {1:m book, translator_id}
 */
class Author extends Orm\Entity {}

Tím, že meta informace jsou v vždycky v {} tak je snadno odlišíš od normálního komentáře. Navíc můžeš mít i víc typů meta dat.

/**
 * @property Author $author {m:1 author_id, book} autor knihy
 * @property string $status {enum self::STATUS_PUBLISHED, self::STATUS_PLANNED} {default self::STATUS_PLANNED}
 */
class Book extends Orm\Entity
{
	const STATUS_PUBLISHED = 'published';
	const STATUS_PLANNED = 'planned';
}
thunderbuff
Člen | 164
+
0
-

Jakým způsobem lze ORM „nejlépe“ injektovat?

	services:
		selectionFactory: Nextras\Orm\SelectionFactory
		bookDao: BookDao

…ale kam s tímhle? Nějaké

<?php
$selectionFactory->addMap('Author', 'author');
$selectionFactory->addMap('Book', 'book');
$selectionFactory->addMap('Tag', 'tag');
?>

Editoval thunderbuff (29. 4. 2013 0:25)

enumag
Člen | 2118
+
0
-

@thunderbuff Přece do configu ke službě nette.database.whatever.selectionFactory, do setup:.

thunderbuff
Člen | 164
+
0
-

Ha! proto je lepší nepouštět se do ničeho po půlnoci!
@enumag: Díky za radu, mělo mě to napadnout :-)

hrach
Člen | 1844
+
0
-

thunderbuff: prosim te, hlavne si s tim jen hraj, rozhodne na tom nic nestav :D

thunderbuff
Člen | 164
+
0
-

Jen tak to zkoušim ;-) BTW: DAO je ošklivý výraz, repository (i když možná méně vystihuje podstatu věci) zní lépe :-P

Editoval thunderbuff (29. 4. 2013 0:54)

hrach
Člen | 1844
+
0
-

Bude repository :)) protoze to predelavam, aby to bylo repository. :D

thunderbuff
Člen | 164
+
0
-

Až ti vyjde čas, mohl bys prosím integrovat instalaci composerem?

thunderbuff
Člen | 164
+
0
-

Pokud chci pouze spočítat prvky v kolekci a nepotřebuji s nimi pracovat, vytáhnou se i tak data z celé tabulky a spočítá se pole. Možná by stálo za implementaci počítání přes count query.

hrach
Člen | 1844
+
0
-

Prosim prosim, ber to jako jenom ukazku. Cele to chci naprosto kompletne predelat.
O tomto vim, schvalne jsem to odstranil (YetORM) to ma, protoze si myslim, ze by na takovouto akci mela byt metoda zvlast. Bohuzel je to hlavne kvuli hloupemu php chovani, kdy countable vyzaduje metodu count.
Ten nazev metody je spatny, protoze je pak $collection->count() nutne count nad vyfetchovanymi daty.

Jack06
Člen | 168
+
0
-

Btw, nepřemýšlel jsi, nebo nechtěl bys popřemýšlet, že bys to anotacemi spojil s doctrine.
Viděl bych v tom ne jednu výhodu.

  1. generování databáze z entit – podle mě super věc, nemusíš se zaobírat tím samým na úrovni entit a ještě na úrovni sql
  2. kompatibilita a porozumění u lidí, kteří používali a používají doctrine
  3. snadnější přechod z doctrine na NDB či naopak
enumag
Člen | 2118
+
0
-

@Jack06: To má drobnou prerekvizitu, totiž zabudování Neon parseru do Nette reflexe, což se sice už dlouho chystá, ale ještě to nikdo neudělal. Aktuální parser není kompatibilní s formátem co Doctrine používá.

jansfabik
Člen | 193
+
0
-

Napadlo mě, že by každý objekt mohl mít i vlastní třídu pro kolekci. Potom by se dalo místo

	return new EntityCollection($this->related('book_tag'), 'App\Model\Tag', 'tag');

psát jenom

	return new TagCollection($this->related('book_tag'));

přičemž tabulka i datový typ entity by se deklaroval ve třídě TagCollection. Zároveň by se tím otevřela možnost rozšiřovat kolekce o další metody specifické pro daný objekt. Kolekce by také mohly pomocí anotace říct IDE, jakého typu jsou entity, takže by pak i krásně fungovalo našeptávání :)

Edit: Na druhou stranu, tohle by už znamenalo, že pro každý objekt jsou třeba tři třídy (Entity, Collection, Dao). Hodilo by se na to vytvořit i nějaký generátor. :)

Editoval jansfabik (3. 5. 2013 17:16)

enumag
Člen | 2118
+
0
-

jansfabik napsal(a):
Napadlo mě, že by každý objekt mohl mít i vlastní třídu pro kolekci.

Můžeš uvést use-case, kdy bys ty vlastní metody u Collection potřeboval?

jansfabik
Člen | 193
+
0
-

Tak třeba jak máš v BookDao metodu findByTags(), tak ta by se dala přesunout do BookCollection pod názvem whereTags($name):

public function whereTags($name)
{
	$this->selection->where('book_tag:tag.name', (array) $name);
	return $this;
}

Kdybych měl těch filtrovacích metod v Dao víc (findByTags(), findByWhatever()), tak bych je nemohl řetězit. U kolekcí mohu findAll()->whereTags(...)->whereWhatever(...).

Edit: A dalším velmi užitečným přínosem je našeptávání v IDE.

Edit 2: A taky mnohem kratší kód oproti

return new EntityCollection($this->related('book_tag'), 'App\Model\Tag', 'tag');

Editoval jansfabik (3. 5. 2013 17:34)

jansfabik
Člen | 193
+
0
-

A pak mě ještě napadlo, že by entita mohla mapper mohl automaticky převádět camelCase na underscore_case (dalo by se to samozřejmě i vypnout pro zpětnou kompatibilitu). Je to divné mít někde $person->fullName a jinde zase $book->author_id, bylo by lepší to sjednotit.

Editoval jansfabik (3. 5. 2013 23:13)

uestla
Backer | 799
+
0
-

Tohle chování se (vy)řešilo :-)

hrach
Člen | 1844
+
0
-

Muj orm urcite nebude podporovat mapovani sloupcu na entite do databaze, respektive mapperu. to si ma prave resit mapper.

enumag
Člen | 2118
+
0
-

@hrach: Jak plánuješ další vývoj? Máš někde nějakou roadmap? Chci si o prázdninách začít hrát s nějakým ORM, ale pořád nevím se kterým. Tohle je sice horký kandidát, ale zase se už dva měsíce nehnulo z místa, tak váhám.

hrach
Člen | 1844
+
0
-

Vcera sem udelal Bc. ;) tot duvod. Jinak toto je mrtve, bude v brzke dobe uplna nahrada :)

enumag
Člen | 2118
+
0
-

Aha, tím se to vysvětluje. Gratuluji! ;-)

nanuqcz
Člen | 822
+
0
-

Je to schválně, že nemůžu „nextras/orm“ najít na Packagistovi?
http://easycaptures.com/…61512140.jpg

Díky

Jan Tvrdík
Nette guru | 2595
+
0
-

ano

hrach
Člen | 1844
+
0
-

@nanuqcz ano, toto byl jednodenni pokus :) napisu to na zacatek vlakna..

elektricman
Člen | 29
+
0
-

Ahoj,
zkouším tvoje Nextras/ORM (btw parádní práce, konečně něco co má „interfejs“ jak si ho u orm představuju :))
a narazil sem na problém :(..

jak vytáhnout data z kolekce podle podmínky např. (pole ve findBy sem si právě vycucal z prstu, protože práve nevim jak to udělat):

$this->orm->peoples->findBy(['TIMESTAMPDIFF(YEAR, birthdate, NOW) > ?' => 18]); // věk větší než 18
nebo
$this->orm->peoples->findBy(['this->group->name LIKE %?% OR this->team->name LIKE %?%'=>['whatever','whatever'])); // skupina nebo pozice člověka obsahuje text whatever

Prostě nějaký filtry. Podle zdrojáku to vypadá že to z podmínek prostě umí parsovat jen to „this->“, ale ne normální SQL syntaxi.

Chápu že tohle by se asi nemělo řešit na urovni presentru, ale ani v repository nemám přístup přímo k SQL query, a do mapperu se to samozžejmě nehodí .. .

Editoval elektricman (5. 7. 2014 9:24)

hrach
Člen | 1844
+
+1
-

vytahnout data z kolekce

to neni uplne tak dofiltrovani kolekce, jako spis proste najit v db nejaky entity, pokud mas takovyto slozitejsi dotaz, nejlepsim resenim je to presunout na vrstvu mapperu – je to v poradku. tj.

PeopleRepository

/**
 * @method Nextras\Orm\Entity\ICollection|Person[] findByAge(int $age)
 */
class PeopleRepository extends Nextras\Orm\Repository\Repository
{}

PoepleMapper

class PeopleMapper exntends Nextras\Orm\Mapper\Mapper
{

	public function findByAge($age)
	{
		return $this->databaseContext->query('SELECT * FROM persons WHERE ...');
	}

}

je to na Mapper vrstve resene proto, ze pouzivas pro filtrovani vyrazy, ktere samozrejme maji neco spolecneho s sql, tj. repository vrstva o nich nema ani paru.

btw, shoudl work inflection Person → People, https://github.com/…/Inflect.php#L79

hrach
Člen | 1844
+
0
-

Toto vlakno bylo vytvoreni pro jednoduseni pokus Nextras\Orm, po roce jsem ale vydal uplne jine orm se stejnym nazvem, jeho diskuzni vlakno je zde – https://forum.nette.org/…nextras-dbal diky :)