Thanks to a helpful stack overflow response, I now have an improved solution to my problem of needing to add arrays of joined ids to JSON output from CakePHP 3. The output from a test containing Widget models that can each have many Foos looks something like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
{ "widgets": [ { "id": 1, "foo_ids": [ 1 ] }, { "id": 3, "foo_ids": [ ] }, { "id": 2, "foo_ids": [ 1, 2, 3 ] } ] } |
To easily and automatically add this capability to any model I wanted, I created a new class extending Cake’s Table , which I called “ApiTable”:
1 2 3 4 5 6 7 8 9 |
class ApiTable extends Table { public function findForApi (...) { ... } public function formatWithJoinedIds (...) { ... } } |
The formatWithJoinedIds() method adds an array called model_ids for each model, from an input list, that is associated with this one through a ‘BelongsToMany’ or ‘HasMany’ join, without including the contents of the associated models. It is intended to be called from the findApi() method, which simply performs a query containing the passed-in desired list of models and formats the results.
The custom finder is straightforward:
1 2 3 4 5 6 |
public function findForApi (Query $query, array $includeIdsFor) { $query ->contain($includeIdsFor) ->formatResults([$this, 'formatWithJoinedIds']); return $query; } |
The array $includeIdsFor should contain strings identifying the associated properties whose joined ids should be included. For instance, if a Widget has many associated Foos, Bars, and Bazzes, the ids of the foos and bars would be included in the output by passing ['Foos', 'Bars'].
The result formatter, used to add the calculated fields containing the id arrays, is a little longer but not much more complicated:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
public function formatWithJoinedIds ($results, $keepJoinedData) { // Array of all associations this model has many models of $joins = array_merge( $this->associations()->type('BelongsToMany'), $this->associations()->type('HasMany'), [] ); // Map each result row to itself, with the array id fields // added, and optionally foreign model data removed return $results->map( function ($row) use ($joins, $keepJoinedData) { foreach ($joins as $join) { $property = $join->property(); if (isset($row[$property])) { // Start a new id array for this property $ids = []; foreach ($row[$property] as $joinedToRow) { $ids[] = $joinedToRow['id']; } // Turn e.g. 'foos' into 'foo_ids', and add this // as a new property of the row $row[Inflector::singularize($property) . '_ids'] = $ids; if (!$keepJoinedData) { // Optionally remove the associated data unset($row[$property]); } } } return $row; } ); } |
This routine generates an array of all properties of the current model that point to many of another model. It then iterates over these properties, and for each one iterates over all of the foreign models to collect an array of their ids. The resulting array is added as a new property to the row, and the foreign models themselves are unset if they are not to be included in the output.