Search code examples
cakephpldapcakephp-3.0data-retrieval

Cakephp 3 - How to integrate external sources in table?


I working on an application that has its own database and gets user information from another serivce (an LDAP is this case, through an API package).

Say I have a tables called Articles, with a column user_id. There is no Users table, instead a user or set of users is retrieved through the external API:

$user = LDAPConnector::getUser($user_id);
$users = LDAPConnector::getUsers([1, 2, 5, 6]);

Of course I want retrieving data from inside a controller to be as simple as possible, ideally still with something like:

$articles = $this->Articles->find()->contain('Users');

foreach ($articles as $article) {
    echo $article->user->getFullname();
}

I'm not sure how to approach this.

Where should I place the code in the table object to allow integration with the external API?

And as a bonus question: How to minimise the number of LDAP queries when filling the Entities?
i.e. it seems to be a lot faster by first retrieving the relevant users with a single ->getUsers() and placing them later, even though iterating over the articles and using multiple ->getUser() might be simpler.


Solution

  • The most simple solution would be to use a result formatter to fetch and inject the external data.

    The more sophisticated solution would a custom association, and a custom association loader, but given how database-centric associations are, you'd probably also have to come up with a table and possibly a query implementation that handles your LDAP datasource. While it would be rather simple to move this into a custom association, containing the association will look up a matching table, cause the schema to be inspected, etc.

    So I'll stick with providing an example for the first option. A result formatter would be pretty simple, something like this:

    $this->Articles
        ->find()
        ->formatResults(function (\Cake\Collection\CollectionInterface $results) {
            $userIds = array_unique($results->extract('user_id')->toArray());
    
            $users = LDAPConnector::getUsers($userIds);
            $usersMap = collection($users)->indexBy('id')->toArray();
    
            return $results
                ->map(function ($article) use ($usersMap) {
                    if (isset($usersMap[$article['user_id']])) {
                        $article['user'] = $usersMap[$article['user_id']];
                    }
    
                    return $article;
                });
            });
    

    The example makes the assumption that the data returned from LDAPConnector::getUsers() is a collection of associative arrays, with an id key that matches the user id. You'd have to adapt this accordingly, depending on what exactly LDAPConnector::getUsers() returns.

    That aside, the example should be rather self-explanatory, first obtain a unique list of users IDs found in the queried articles, obtain the LDAP users using those IDs, then inject the users into the articles.

    If you wanted to have entities in your results, then create entities from the user data, for example like this:

    $userData = $usersMap[$article['user_id']];
    $article['user'] = new \App\Model\Entity\User($userData);
    

    For better reusability, put the formatter in a custom finder. In your ArticlesTable class:

    public function findWithUsers(\Cake\ORM\Query $query, array $options)
    {
        return $query->formatResults(/* ... */);
    }
    

    Then you can just do $this->Articles->find('withUsers'), just as simple as containing.

    See also