New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BelongsToMany přes dva sloupce #53

Closed
echo511 opened this Issue Apr 27, 2014 · 5 comments

Comments

Projects
None yet
3 participants
@echo511

echo511 commented Apr 27, 2014

Zdravím,
mám entitu Node (id, label) a Edge (id, source, target, label). Edge reprezentuje vazbu mezi dvěma Node.

V Node chci metodu getEdges(), která mi vrátí Edge[] seřazené dle mých kritérií.

Dotaz pro konkrétní Node s id 1 by vypadal takto:

SELECT * FROM edge WHERE source = 1 OR target = 1 ORDER BY label

Zároveň nechci přijít o NotORM princip. Jak toto prosím v LeanMapperu řešit?
Děkuji

@Tharos Tharos added the Question label Apr 27, 2014

@Tharos

This comment has been minimized.

Show comment
Hide comment
@Tharos

Tharos Apr 28, 2014

Owner

Vyřešil jsem to nad databází z Lean Mapper testů, kde z knihy vedou dvě hasOne vazby na autora (autor a recenzent). V ukázkách tedy budu pro každého autora hledat všechny knihy, které s ním nějak souvisí, tj. které mají ID dané knihy v author_id nebo reviewer_id.

Vím v principu o dvou možnostech, jak se s tím vypořádat…

První možnost – seřazení v paměti

Pokud bych potřeboval de facto sjednotit hodnoty ze dvou vztahů a navíc je pak už pouze seřadit, já osobně bych asi neváhal a udělal bych to v paměti:

/**
 * @property int $id
 */
class Entity extends \LeanMapper\Entity
{

    /**
     * @return string
     */
    public function __toString()
    {
        return get_called_class() . '#' . $this->id;
    }

}

/**
 * @property string $name
 * @property Book[] $authoredBooks m:belongsToMany
 * @property Book[] $reviewedBooks m:belongsToMany(reviewer_id)
 * @property Book[] $books m:useMethods
 */
class Author extends Entity
{

    /**
     * @return Book[]
     */
    public function getBooks()
    {
        $books = array_unique(array_merge($this->authoredBooks, $this->reviewedBooks));
        usort($books, function (Book $a, Book $b) {
            return strcoll($a->name, $b->name);
        });
        return $books;
    }

}

/**
 * @property string $name
 */
class Book extends Entity
{
}

class AuthorRepository extends \LeanMapper\Repository
{

    /**
     * @return Author[]
     */
    public function findAll()
    {
        return $this->createEntities(
            $this->createFluent()->fetchAll()
        );
    }

}

Takovéto řešení při následujícím použití:

$authorRepository = new AuthorRepository($connection, $mapper, $entityFactory);

foreach ($authorRepository->findAll() as $book) {
    echo $book->name, "\n";
    foreach ($book->books as $book) {
        echo "\t- $book->name\n";
    }
}

Zavolá následující dotazy:

SELECT [author].* FROM [author]
SELECT [book].* FROM [book] WHERE [book].[author_id] IN (1, 2, 3, 4, 5)
SELECT [book].* FROM [book] WHERE [book].[reviewer_id] IN (1, 2, 3, 4, 5)

Druhá možnost – „lazy preloading“

Pokud bych opravdu chtěl, aby ta sjednocená související data seřadila databáze (a aby se získala jedním dotazem :-P), je zapotřebí uvědomit si následující věci:

  • V Lean Mapperu není problém použitím nějakého filtru ta potřebná data načíst jedním dotazem (ve verzi 2.2.0 to lze), ale je problém takto získaný výsledek provázat s hlavním výsledkem. Lean Mapper aktuálně prostě neumí navázat „referencing row“ podle více sloupců, navíc ještě pomocí OR.
  • Od verze 2.2.0 ale lze ty výsledky provázat ručně ($mainResult->setReferencingResult($referencingResult, ...)). Stále ale platí, že ve výsledku musí být propojení vyjádřené jedním sloupcem, takže je nutné vytvořit nějaký virtuální v paměti, přes který se poté výsledky prováží (třeba related_person). Ty řádky, které se provazují přes author_id a reviewer_id na různé autory zároveň musí být ve výsledku přítomny dvakrát, přičemž jednou bude related_person nabývat hodnoty author_id a podruhé hodnoty reviewer_id.
  • Jelikož se tímto zdvojováním v resultu objeví řádky se stejnou hodnotou PK, což je rozhodně nestandardní, nelze ten související result vytvořit přes Result::createInstance($dataWithDoubledRows, ...), protože ten by ta zdánlivě duplicitní data zahodil, nýbrž musí se použít fígl v podobě vytvořený Result::createInstance([], ...) s tím, že se pak ten result naplní přes Result::addDataEntry.
  • No a nakonec je nutné uvědomit si, že tohle vše nelze kompletně provést v getteru Author::getBooks, protože tam už se přímo k resultům nedostaneme (jsou důsledně zapouzdřené v Row). Je nutné si při vytváření takového resultu vytvořit nějaký „preloader“, který to preloading on-demand provede.
  • Profit

Výsledek vypadá následovně:

class BooksLoader
{

    /** @var Result */
    private $result;

    /** @var Connection */
    private $connection;

    /** @var bool */
    private $isPreloaded = false;

    /** @var IMapper */
    private $mapper;


    /**
     * @param Result $result
     * @param Connection $connection
     * @param IMapper $mapper
     */
    public function __construct(Result $result, Connection $connection, IMapper $mapper)
    {
        $this->result = $result;
        $this->connection = $connection;
        $this->mapper = $mapper;
    }

    public function preloadBooks()
    {
        if (!$this->isPreloaded) {
            $idsIndex = [];
            foreach ($this->result as $entry) {
                $idsIndex[$entry['id']] = true;
            }
            $books = $this->connection->select('*')
                    ->from('book')
                    ->where('[author_id] IN %in OR [reviewer_id] IN %in', $ids = array_keys($idsIndex), $ids)
                    ->orderBy('name')
                    ->fetchAll();

            $referencing = Result::createInstance([], 'book', $this->connection, $this->mapper, 'book(related_person)');
            foreach ($books as $book) {
                if (isset($idsIndex[$book['author_id']]) or isset($idsIndex[$book['reviewer_id']])) {
                    $book = $book->toArray();
                    $book['related_person'] = $book['author_id'];
                    $referencing->addDataEntry($book);
                    if ($book['reviewer_id'] !== $book['author_id'] and $book['reviewer_id'] !== null) {
                        $book['related_person'] = $book['reviewer_id'];
                        $referencing->addDataEntry($book); // making of „duplicities“
                    }
                }
            }
            $referencing->cleanAddedAndRemovedMeta();
            $this->result->setReferencingResult($referencing, 'book', 'related_person');

            $this->isPreloaded = true;
        }
    }

}

/**
 * @property int $id
 */
class Entity extends \LeanMapper\Entity
{
}

/**
 * @property string $name
 * @property Book[] $books m:belongsToMany(related_person)
 */
class Author extends Entity
{

    /** @var BooksLoader */
    private $booksLoader;


    /**
     * @param BooksLoader $booksLoader
     */
    public function injectBooksLoader(BooksLoader $booksLoader)
    {
        $this->booksLoader = $booksLoader;
    }

    /**
     * @return Book[]
     */
    public function getBooks()
    {
        $this->booksLoader->preloadBooks();
        return $this->getValueByPropertyWithRelationship('books');
    }

}

/**
 * @property string $name
 */
class Book extends Entity
{
}

class AuthorRepository extends \LeanMapper\Repository
{

    /**
     * @return Author[]
     */
    public function findAll()
    {
        $rows = $this->createFluent()->fetchAll();
        $result = Result::createInstance($rows, 'author', $this->connection, $this->mapper);

        $booksLoader = new BooksLoader($result, $this->connection, $this->mapper);

        $authors = [];
        foreach ($rows as $dibiRow) {
            $row = $result->getRow($dibiRow->id);
            $author = $this->entityFactory->createEntity(Author::class, $row);
            $author->makeAlive($this->entityFactory);
            $author->injectBooksLoader($booksLoader);
            $authors[] = $author;
        }
        return $authors;
    }

}

Dotazy při stejném použití jako v prvním řešení jsou následující:

SELECT [author].* FROM [author]
SELECT * FROM [book] WHERE [author_id] IN (1, 2, 3, 4, 5) OR [reviewer_id] IN (1, 2, 3, 4, 5) ORDER BY [name]

Druhé předvedené je dost advanced řešení a asi bych nedoporučil dělat to takhle, dokud by pro to nebyl nějaký ultra dobrý důvod (třeba seřazení nějakým speciálním způsobem, který umí jenom databáze, atp.).

Schválně jsem si s ním ale pohrál, protože když se to zobecní, jde o ukázku, jak lze v Lean Mapperu docela lazy a při zachování „NotORM principu“ načíst při traverzování mezi entitami de facto libovolná data. Jen je o něco více práce s jejich propojením.

Těm, kdo to pochopili, gratuluju, protože rozumí Lean Mapperu minimálně tak dobře, jako já. :)

Owner

Tharos commented Apr 28, 2014

Vyřešil jsem to nad databází z Lean Mapper testů, kde z knihy vedou dvě hasOne vazby na autora (autor a recenzent). V ukázkách tedy budu pro každého autora hledat všechny knihy, které s ním nějak souvisí, tj. které mají ID dané knihy v author_id nebo reviewer_id.

Vím v principu o dvou možnostech, jak se s tím vypořádat…

První možnost – seřazení v paměti

Pokud bych potřeboval de facto sjednotit hodnoty ze dvou vztahů a navíc je pak už pouze seřadit, já osobně bych asi neváhal a udělal bych to v paměti:

/**
 * @property int $id
 */
class Entity extends \LeanMapper\Entity
{

    /**
     * @return string
     */
    public function __toString()
    {
        return get_called_class() . '#' . $this->id;
    }

}

/**
 * @property string $name
 * @property Book[] $authoredBooks m:belongsToMany
 * @property Book[] $reviewedBooks m:belongsToMany(reviewer_id)
 * @property Book[] $books m:useMethods
 */
class Author extends Entity
{

    /**
     * @return Book[]
     */
    public function getBooks()
    {
        $books = array_unique(array_merge($this->authoredBooks, $this->reviewedBooks));
        usort($books, function (Book $a, Book $b) {
            return strcoll($a->name, $b->name);
        });
        return $books;
    }

}

/**
 * @property string $name
 */
class Book extends Entity
{
}

class AuthorRepository extends \LeanMapper\Repository
{

    /**
     * @return Author[]
     */
    public function findAll()
    {
        return $this->createEntities(
            $this->createFluent()->fetchAll()
        );
    }

}

Takovéto řešení při následujícím použití:

$authorRepository = new AuthorRepository($connection, $mapper, $entityFactory);

foreach ($authorRepository->findAll() as $book) {
    echo $book->name, "\n";
    foreach ($book->books as $book) {
        echo "\t- $book->name\n";
    }
}

Zavolá následující dotazy:

SELECT [author].* FROM [author]
SELECT [book].* FROM [book] WHERE [book].[author_id] IN (1, 2, 3, 4, 5)
SELECT [book].* FROM [book] WHERE [book].[reviewer_id] IN (1, 2, 3, 4, 5)

Druhá možnost – „lazy preloading“

Pokud bych opravdu chtěl, aby ta sjednocená související data seřadila databáze (a aby se získala jedním dotazem :-P), je zapotřebí uvědomit si následující věci:

  • V Lean Mapperu není problém použitím nějakého filtru ta potřebná data načíst jedním dotazem (ve verzi 2.2.0 to lze), ale je problém takto získaný výsledek provázat s hlavním výsledkem. Lean Mapper aktuálně prostě neumí navázat „referencing row“ podle více sloupců, navíc ještě pomocí OR.
  • Od verze 2.2.0 ale lze ty výsledky provázat ručně ($mainResult->setReferencingResult($referencingResult, ...)). Stále ale platí, že ve výsledku musí být propojení vyjádřené jedním sloupcem, takže je nutné vytvořit nějaký virtuální v paměti, přes který se poté výsledky prováží (třeba related_person). Ty řádky, které se provazují přes author_id a reviewer_id na různé autory zároveň musí být ve výsledku přítomny dvakrát, přičemž jednou bude related_person nabývat hodnoty author_id a podruhé hodnoty reviewer_id.
  • Jelikož se tímto zdvojováním v resultu objeví řádky se stejnou hodnotou PK, což je rozhodně nestandardní, nelze ten související result vytvořit přes Result::createInstance($dataWithDoubledRows, ...), protože ten by ta zdánlivě duplicitní data zahodil, nýbrž musí se použít fígl v podobě vytvořený Result::createInstance([], ...) s tím, že se pak ten result naplní přes Result::addDataEntry.
  • No a nakonec je nutné uvědomit si, že tohle vše nelze kompletně provést v getteru Author::getBooks, protože tam už se přímo k resultům nedostaneme (jsou důsledně zapouzdřené v Row). Je nutné si při vytváření takového resultu vytvořit nějaký „preloader“, který to preloading on-demand provede.
  • Profit

Výsledek vypadá následovně:

class BooksLoader
{

    /** @var Result */
    private $result;

    /** @var Connection */
    private $connection;

    /** @var bool */
    private $isPreloaded = false;

    /** @var IMapper */
    private $mapper;


    /**
     * @param Result $result
     * @param Connection $connection
     * @param IMapper $mapper
     */
    public function __construct(Result $result, Connection $connection, IMapper $mapper)
    {
        $this->result = $result;
        $this->connection = $connection;
        $this->mapper = $mapper;
    }

    public function preloadBooks()
    {
        if (!$this->isPreloaded) {
            $idsIndex = [];
            foreach ($this->result as $entry) {
                $idsIndex[$entry['id']] = true;
            }
            $books = $this->connection->select('*')
                    ->from('book')
                    ->where('[author_id] IN %in OR [reviewer_id] IN %in', $ids = array_keys($idsIndex), $ids)
                    ->orderBy('name')
                    ->fetchAll();

            $referencing = Result::createInstance([], 'book', $this->connection, $this->mapper, 'book(related_person)');
            foreach ($books as $book) {
                if (isset($idsIndex[$book['author_id']]) or isset($idsIndex[$book['reviewer_id']])) {
                    $book = $book->toArray();
                    $book['related_person'] = $book['author_id'];
                    $referencing->addDataEntry($book);
                    if ($book['reviewer_id'] !== $book['author_id'] and $book['reviewer_id'] !== null) {
                        $book['related_person'] = $book['reviewer_id'];
                        $referencing->addDataEntry($book); // making of „duplicities“
                    }
                }
            }
            $referencing->cleanAddedAndRemovedMeta();
            $this->result->setReferencingResult($referencing, 'book', 'related_person');

            $this->isPreloaded = true;
        }
    }

}

/**
 * @property int $id
 */
class Entity extends \LeanMapper\Entity
{
}

/**
 * @property string $name
 * @property Book[] $books m:belongsToMany(related_person)
 */
class Author extends Entity
{

    /** @var BooksLoader */
    private $booksLoader;


    /**
     * @param BooksLoader $booksLoader
     */
    public function injectBooksLoader(BooksLoader $booksLoader)
    {
        $this->booksLoader = $booksLoader;
    }

    /**
     * @return Book[]
     */
    public function getBooks()
    {
        $this->booksLoader->preloadBooks();
        return $this->getValueByPropertyWithRelationship('books');
    }

}

/**
 * @property string $name
 */
class Book extends Entity
{
}

class AuthorRepository extends \LeanMapper\Repository
{

    /**
     * @return Author[]
     */
    public function findAll()
    {
        $rows = $this->createFluent()->fetchAll();
        $result = Result::createInstance($rows, 'author', $this->connection, $this->mapper);

        $booksLoader = new BooksLoader($result, $this->connection, $this->mapper);

        $authors = [];
        foreach ($rows as $dibiRow) {
            $row = $result->getRow($dibiRow->id);
            $author = $this->entityFactory->createEntity(Author::class, $row);
            $author->makeAlive($this->entityFactory);
            $author->injectBooksLoader($booksLoader);
            $authors[] = $author;
        }
        return $authors;
    }

}

Dotazy při stejném použití jako v prvním řešení jsou následující:

SELECT [author].* FROM [author]
SELECT * FROM [book] WHERE [author_id] IN (1, 2, 3, 4, 5) OR [reviewer_id] IN (1, 2, 3, 4, 5) ORDER BY [name]

Druhé předvedené je dost advanced řešení a asi bych nedoporučil dělat to takhle, dokud by pro to nebyl nějaký ultra dobrý důvod (třeba seřazení nějakým speciálním způsobem, který umí jenom databáze, atp.).

Schválně jsem si s ním ale pohrál, protože když se to zobecní, jde o ukázku, jak lze v Lean Mapperu docela lazy a při zachování „NotORM principu“ načíst při traverzování mezi entitami de facto libovolná data. Jen je o něco více práce s jejich propojením.

Těm, kdo to pochopili, gratuluju, protože rozumí Lean Mapperu minimálně tak dobře, jako já. :)

@echo511

This comment has been minimized.

Show comment
Hide comment
@echo511

echo511 Apr 28, 2014

Díky za rozsáhlý a poučný komentář.

echo511 commented Apr 28, 2014

Díky za rozsáhlý a poučný komentář.

@echo511 echo511 closed this Apr 28, 2014

@achtan

This comment has been minimized.

Show comment
Hide comment
@achtan

achtan Apr 28, 2014

OT: @echo511 a grafove databazy poznas ?

achtan commented Apr 28, 2014

OT: @echo511 a grafove databazy poznas ?

@echo511

This comment has been minimized.

Show comment
Hide comment
@echo511

echo511 Apr 28, 2014

@achtan Jo, znám. Perfektně by se hodily na můj projekt, ale pokud to budu chtit spustit na hostingu, tak na neo4j nenarazim. Proto píšu implementaci i pro MySQL, potažmo LeanMapper.

echo511 commented Apr 28, 2014

@achtan Jo, znám. Perfektně by se hodily na můj projekt, ale pokud to budu chtit spustit na hostingu, tak na neo4j nenarazim. Proto píšu implementaci i pro MySQL, potažmo LeanMapper.

@Tharos

This comment has been minimized.

Show comment
Hide comment
@Tharos

Tharos Apr 28, 2014

Owner

Ještě celý dnešní den jsem tenhle problém nedokázal dostat z hlavy a dal jsem dohromady ještě o něco elegantnější řešení… :)

class AuthorsResultProxy extends \LeanMapper\ResultProxy
{

    /** @var bool */
    private $hasPreloadedBooks = false;


    public function markBooksAsPreloaded()
    {
        $this->hasPreloadedBooks = true;
    }

    /**
     * @return bool
     */
    public function hasPreloadedBooks()
    {
        return $this->hasPreloadedBooks;
    }

}

class BooksLoader
{

    /** @var Connection */
    private $connection;

    /** @var IMapper */
    private $mapper;


    /**
     * @param Connection $connection
     * @param IMapper $mapper
     */
    public function __construct(Connection $connection, IMapper $mapper)
    {
        $this->connection = $connection;
        $this->mapper = $mapper;
    }

    /**
     * @param ResultProxy $resultProxy
     */
    public function preloadBooks(ResultProxy $resultProxy)
    {
        $idsIndex = [];
        foreach ($resultProxy as $entry) {
            $idsIndex[$entry['id']] = true;
        }
        $books = $this->connection->select('*')
                ->from('book')
                ->where('[author_id] IN %in OR [reviewer_id] IN %in', $ids = array_keys($idsIndex), $ids)
                ->orderBy('name')
                ->fetchAll();

        $referencing = Result::createInstance([], 'book', $this->connection, $this->mapper, 'book(related_person)');
        foreach ($books as $book) {
            if (isset($idsIndex[$book['author_id']]) or isset($idsIndex[$book['reviewer_id']])) {
                $book = $book->toArray();
                $book['related_person'] = $book['author_id'];
                $referencing->addDataEntry($book);
                if ($book['reviewer_id'] !== $book['author_id'] and $book['reviewer_id'] !== null) {
                    $book['related_person'] = $book['reviewer_id'];
                    $referencing->addDataEntry($book);
                }
            }
        }
        $referencing->cleanAddedAndRemovedMeta();
        $resultProxy->setReferencingResult($referencing, 'book', 'related_person');
        $resultProxy->markBooksAsPreloaded();
    }

}

class EntityFactory extends \LeanMapper\DefaultEntityFactory
{

    /** @var BooksLoader */
    private $booksLoader;


    /**
     * @param BooksLoader $booksLoader
     */
    public function __construct(BooksLoader $booksLoader)
    {
        $this->booksLoader = $booksLoader;
    }

    /*
     * @inheritdoc
     */
    public function createEntity($entityClass, $arg = null)
    {
        $entity = parent::createEntity($entityClass, $arg);
        if ($entity instanceof Author and $arg instanceof Row) {
            $entity->injectBooksLoader($this->booksLoader);
        }
        return $entity;
    }

}

/**
 * @property int $id
 */
class Entity extends \LeanMapper\Entity
{
}

/**
 * @property string $name
 * @property Book[] $authoredBooks m:belongsToMany
 * @property Book[] $books m:belongsToMany(related_person)
 */
class Author extends Entity
{

    /** @var BooksLoader */
    private $booksLoader;


    /**
     * @param BooksLoader $booksLoader
     */
    public function injectBooksLoader(BooksLoader $booksLoader)
    {
        $this->booksLoader = $booksLoader;
    }

    /**
     * @return Book[]
     */
    public function getBooks()
    {
        $resultProxy = $this->row->getResultProxy(AuthorsResultProxy::class);
        if (!$resultProxy->hasPreloadedBooks()) {
            $this->booksLoader->preloadBooks($resultProxy);
        }
        return $this->getValueByPropertyWithRelationship('books');
    }

}

/**
 * @property string $name
 */
class Book extends Entity
{
}

class AuthorRepository extends \LeanMapper\Repository
{

    /**
     * @return Author[]
     */
    public function findAll()
    {
        return $this->createEntities(
            $this->createFluent()->fetchAll()
        );
    }

}

Příklad použití je pak následující:

$booksLoader = new BooksLoader($connection, $mapper);
$entityFactory = new EntityFactory($booksLoader);

$authorRepository = new AuthorRepository($connection, $mapper, $entityFactory);

foreach ($authorRepository->findAll() as $book) {
    echo $book->name, "\n";
    foreach ($book->books as $book) {
        echo "\t- $book->name\n";
    }
}

Tohle řešení má jednu zásadní výhodu. Tím injektováním BooksLoader se nezaplevuje repositář, ale děje se to v EntityFactory, kde se to správně má dít. V předchozím řešení díky tomu nefungovalo volání getBooks u autorů, ke kterým jste se například nějak dotraverzovali, což byl zásadní nedostatek.

Tohle poslední řešení funguje ale pouze nad následující develop větví.

/Uf, konečně tuhle věc můžu pustit z hlavy :)…/

Owner

Tharos commented Apr 28, 2014

Ještě celý dnešní den jsem tenhle problém nedokázal dostat z hlavy a dal jsem dohromady ještě o něco elegantnější řešení… :)

class AuthorsResultProxy extends \LeanMapper\ResultProxy
{

    /** @var bool */
    private $hasPreloadedBooks = false;


    public function markBooksAsPreloaded()
    {
        $this->hasPreloadedBooks = true;
    }

    /**
     * @return bool
     */
    public function hasPreloadedBooks()
    {
        return $this->hasPreloadedBooks;
    }

}

class BooksLoader
{

    /** @var Connection */
    private $connection;

    /** @var IMapper */
    private $mapper;


    /**
     * @param Connection $connection
     * @param IMapper $mapper
     */
    public function __construct(Connection $connection, IMapper $mapper)
    {
        $this->connection = $connection;
        $this->mapper = $mapper;
    }

    /**
     * @param ResultProxy $resultProxy
     */
    public function preloadBooks(ResultProxy $resultProxy)
    {
        $idsIndex = [];
        foreach ($resultProxy as $entry) {
            $idsIndex[$entry['id']] = true;
        }
        $books = $this->connection->select('*')
                ->from('book')
                ->where('[author_id] IN %in OR [reviewer_id] IN %in', $ids = array_keys($idsIndex), $ids)
                ->orderBy('name')
                ->fetchAll();

        $referencing = Result::createInstance([], 'book', $this->connection, $this->mapper, 'book(related_person)');
        foreach ($books as $book) {
            if (isset($idsIndex[$book['author_id']]) or isset($idsIndex[$book['reviewer_id']])) {
                $book = $book->toArray();
                $book['related_person'] = $book['author_id'];
                $referencing->addDataEntry($book);
                if ($book['reviewer_id'] !== $book['author_id'] and $book['reviewer_id'] !== null) {
                    $book['related_person'] = $book['reviewer_id'];
                    $referencing->addDataEntry($book);
                }
            }
        }
        $referencing->cleanAddedAndRemovedMeta();
        $resultProxy->setReferencingResult($referencing, 'book', 'related_person');
        $resultProxy->markBooksAsPreloaded();
    }

}

class EntityFactory extends \LeanMapper\DefaultEntityFactory
{

    /** @var BooksLoader */
    private $booksLoader;


    /**
     * @param BooksLoader $booksLoader
     */
    public function __construct(BooksLoader $booksLoader)
    {
        $this->booksLoader = $booksLoader;
    }

    /*
     * @inheritdoc
     */
    public function createEntity($entityClass, $arg = null)
    {
        $entity = parent::createEntity($entityClass, $arg);
        if ($entity instanceof Author and $arg instanceof Row) {
            $entity->injectBooksLoader($this->booksLoader);
        }
        return $entity;
    }

}

/**
 * @property int $id
 */
class Entity extends \LeanMapper\Entity
{
}

/**
 * @property string $name
 * @property Book[] $authoredBooks m:belongsToMany
 * @property Book[] $books m:belongsToMany(related_person)
 */
class Author extends Entity
{

    /** @var BooksLoader */
    private $booksLoader;


    /**
     * @param BooksLoader $booksLoader
     */
    public function injectBooksLoader(BooksLoader $booksLoader)
    {
        $this->booksLoader = $booksLoader;
    }

    /**
     * @return Book[]
     */
    public function getBooks()
    {
        $resultProxy = $this->row->getResultProxy(AuthorsResultProxy::class);
        if (!$resultProxy->hasPreloadedBooks()) {
            $this->booksLoader->preloadBooks($resultProxy);
        }
        return $this->getValueByPropertyWithRelationship('books');
    }

}

/**
 * @property string $name
 */
class Book extends Entity
{
}

class AuthorRepository extends \LeanMapper\Repository
{

    /**
     * @return Author[]
     */
    public function findAll()
    {
        return $this->createEntities(
            $this->createFluent()->fetchAll()
        );
    }

}

Příklad použití je pak následující:

$booksLoader = new BooksLoader($connection, $mapper);
$entityFactory = new EntityFactory($booksLoader);

$authorRepository = new AuthorRepository($connection, $mapper, $entityFactory);

foreach ($authorRepository->findAll() as $book) {
    echo $book->name, "\n";
    foreach ($book->books as $book) {
        echo "\t- $book->name\n";
    }
}

Tohle řešení má jednu zásadní výhodu. Tím injektováním BooksLoader se nezaplevuje repositář, ale děje se to v EntityFactory, kde se to správně má dít. V předchozím řešení díky tomu nefungovalo volání getBooks u autorů, ke kterým jste se například nějak dotraverzovali, což byl zásadní nedostatek.

Tohle poslední řešení funguje ale pouze nad následující develop větví.

/Uf, konečně tuhle věc můžu pustit z hlavy :)…/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment