johnnyfreeman / laravel-custom-relation

A custom relation for when stock relations aren't enough.
78 stars 28 forks source link

* Eager loading matcher offloaded to caller #8

Closed nerdo closed 4 years ago

nerdo commented 4 years ago

This should address #3 where match() was handled by the custom relation class.

Instead, the responsibility is offloaded to the caller which is responsible for providing a eager loading matcher closure.

Since it is the caller that should know about the keys and should know how to match models, it should be doing the work rather than trying to provide a generic one-size-fits-all matcher.

If there is a common scenario, maybe a default matcher can be provided, but the caller should be allowed to provide its own.

johnnyfreeman commented 4 years ago

I like this idea! Can you provide an example of it's usage?

nerdo commented 4 years ago

I have a bit of a complex example that I am working with where I'm working with two (potential) foreign keys from different systems. It may not be the best example, but I did get it to work. When I have more time, I might be able to create a simpler use case:

A more complex example with eager loading.

In this scenario there is a Client model with notes, but the notes may be attributed to the client using the client_id or an external crm_id, so a custom relation defines the relationship.

use LaravelCustomRelation\HasCustomRelations;

class Client
{
    use HasCustomRelations;

    public function notes()
    {
        $notesTable = (new Note)->getTable();
        $clientsTable = $this->getTable();

        return $this->customRelation(
            Note::class,

            // constraints
            function ($relation) use ($notesTable, $clientsTable) {
                $relation
                    ->getQuery()
                    ->select($notesTable . '.*')
                    ->join(
                        $clientsTable,
                        function ($join) use ($clientsTable, $notesTable) {
                            $join
                                ->on($clientsTable . '.client_id', '=', $notesTable . '.client_id')
                                ->orOn($clientsTable . '.crm_id', '=', $notesTable . '.crm_id');
                            if ($this->id) {
                                $join->where($clientsTable . '.id', '=', $this->id);
                            }
                        }
                    );
            },

            // eager constraints
            function ($relation, $models) use ($clientsTable) {
                $clients = collect($models);
                $relation
                    ->getQuery()
                    ->whereIn(
                        $clientsTable . '.client_id',
                        $clients
                            ->map(function ($client) {
                                return $client->client_id;
                            })
                            ->filter()
                            ->values()
                            ->all()
                    )
                    ->orWhereIn(
                        $clientsTable . '.crm_id',
                        $clients
                            ->map(function ($client) {
                                return $client->crm_id;
                            })
                            ->filter()
                            ->values()
                            ->all()
                    );
            },

            // eager matcher
            function (array $models, \Illuminate\Support\Collection $results, $relation, $customRelation) {
                $buildDictionary = function (\Illuminate\Support\Collection $results) {
                    return $results
                        ->reduce(
                            function ($dictionary, $current) {
                                if (!($current instanceof Note)) {
                                    return $dictionary;
                                }
                                if ($current->client_id) {
                                    $key = 'client_id_' . $current->client_id;
                                    $dictionary[$key] = $dictionary[$key] ?? [];
                                    $dictionary[$key][] = $current;
                                }
                                if ($current->crm_id) {
                                    $key = 'crm_id_' . $current->crm_id;
                                    $dictionary[$key] = $dictionary[$key] ?? [];
                                    $dictionary[$key][] = $current;
                                }
                                return $dictionary;
                            },
                            []
                        );
                };

                $dictionary = collect($buildDictionary($results));
                $related = $customRelation->getRelated();

                // Once we have the dictionary we can simply spin through the parent models to
                // link them up with their children using the keyed dictionary to make the
                // matching very convenient and easy work. Then we'll just return them.
                foreach ($models as $model) {
                    $cKey = 'client_id_' . $model->getAttribute('client_id');
                    $crmKey = 'crm_id_' . $model->getAttribute('crm_id');

                    $matches = $dictionary
                        ->filter(function ($value, $key) use ($cKey, $crmKey) {
                            return $key === $cKey || $key === $crmKey;
                        })
                        ->values()
                        ->flatten()
                        ->unique()
                        ->all();

                    if ($matches) {
                        $model->setRelation($relation, $related->newCollection($matches));
                    }
                }

                return $models;
            }
        );
    }
}
johnnyfreeman commented 4 years ago

Thanks @nerdo! Merged.