CakeDC Blog

TIPS, INSIGHTS AND THE LATEST FROM THE EXPERTS BEHIND CAKEPHP

Introduction to CakeDC Api plugin

 

The CakeDC API plugin was created with the goal to prepare abstract solutions that solve generic tasks -in case of creating a rest API. It provides such features like automatic rest API generation based on db schema, support nested routes. It also allows the use of different formats like json or xml, and easily adds their own custom format. It helps to solve generic tasks appearing in development of any API, like pagination, data validation, adding common data to response, or building metadata, about data objects.

Dependencies

The CakeDC API plugin hardly depends on the CakeDC Users Plugin. For authentication it is highly recommended to use CakePHP Authentication plugin configured as middleware.

Installation

You can install this plugin into your CakePHP application using composer.

The recommended way to install composer packages is:

composer require cakedc/cakephp-api

 

 Load the Plugin

Ensure  The CakeDC API Plugin is loaded in your src/Aplication.php in bootstrap method.

php

    $this->addPlugin(\CakeDC\Users\Plugin::class);

    $this->addPlugin(\CakeDC\Api\Plugin::class, ['bootstrap' => true, 'routes' => true]);

Configuration

Minimal configuration to allow non authorized requests require you to copy file: ./vendor/cakedc/cakephp-api/config/api_permissions.php.default to ./config/api_permissions.php

Examples

Lets bake table blogs with two fields id and name.

After that, the next requests would be possible to perform to api. Requests would be performed using curl.

Request:

curl http://localhost:8765/api/blogs

Response:

{

    "status": "success",

    "data": [

        {

            "id": 1,

            "name": "blog001"

        }

    ],

    "pagination": {

        "page": 1,

        "limit": 20,

        "pages": 1,

        "count": 1

    },

    "links": [

        {

            "name": "self",

            "href": "http:\/\/localhost:8765\/api\/blogs",

            "rel": "\/api\/blogs",

            "method": "GET"

        },

        {

            "name": "blogs:add",

            "href": "http:\/\/localhost:8765\/api\/blogs",

            "rel": "\/api\/blogs",

            "method": "POST"

        }

    ]

}

Request:

curl -d "name=blog001" -H "Content-Type: application/x-www-form-urlencoded" -X POST http://localhost:8765/api/blogs

Response:

{

    "status": "success",

    "data": {

        "name": "blog001",

        "id": 1

    },

    "links": []

}

Request:

curl -d "name=blog002" -H "Content-Type: application/x-www-form-urlencoded" -X PUT http://localhost:8765/api/blogs/1

Response:

{

    "status": "success",

    "data": {

        "id": 1,

        "name": "blog002"

    },

    "links": []

}

Request:

curl -X DELETE http://localhost:8765/api/blogs/1

Response:

{

    "status": "success",

    "data": true,

    "links": []

}

For more complex features about plugin initialization and configuration based on routes middlewares, we plan to create an additional article.

Services and Actions

In the REST recommendations documents names defined as a noun. Here, services come into play.

It describes business entities. From other side actions define the verbs that describe the operations that should be performed on the actions.

Common and difference between controller classes and services.

The common part is the service is the managing the choosing action to execute.

The primary difference is that service could be nested, if this is defined by request url.

Common and difference between controller actions and service actions.

The common part is the action defined logic of the request.

The primary is that each service’s action is defined as a separate class.

This means that generic actions could be defined as common class and reused in many services.

From the other side, an action class is able to extend if the system has slightly different actions.

This way it is possible to build actions hierarchy.

Both service and actions define an event during this execution flow. 

Main service events:

* Service.beforeDispatch

* Service.beforeProcess

* Service.afterDispatch

Main action events:

* Action.beforeProcess

* Action.onAuth

* Action.beforeValidate

* Action.beforeValidateStopped

* Action.validationFailed

* Action.beforeExecute

* Action.beforeExecuteStopped

* Action.afterProcess

Crud actions define events that depend on the type of action, and more details could be checked in documentation.

* Action.Crud.onPatchEntity

* Action.Crud.onFindEntities

* Action.Crud.afterFindEntities 

* Action.Crud.onFindEntity

Nested services

Consider we have request with method POST /blogs/1/posts with data like {"title": "...", "body": "..."}

As it is possible to see there is nothing in the given data about the blog_id to which the newly created post should belong to.

In the case of controllers we should define custom logic to parse a route, and to consume the blog_id from url.

For nested service all checks and records updates are automatically executed. This will happen for any crud operations, when detected by the route parent service. So for example: GET /blogs/1/posts, will return only posts for the blog with id 1.

Logical checks are also performed, so for request: DELETE /blogs/1/posts/2, a user gets an error if the post with id 2 belongs to the blog with id 2.

Action inheritance

As each action can be defined as a separate class, it is possible to use class inheritance to define common logic. For example:  Add and Edit actions.

Extending services and actions with shared functionality

The alternative way for defining common logic actions is using action extensions. Action extension is a more powerful feature and could be used for global tasks like search or pagination.

It is also possible to create service level extensions. Those extensions work on the top level of the execution process, and could be used for things like adding cors feature, or to append some counter into response.

Add service actions from service::initialize

This is a recommended way to register non crud actions. The mapAction uses the Router class syntax for parsing routes. So on any special use cases well described in cakephp core.

    public function initialize()

    {

        parent::initialize();

        $this->mapAction('view_edit', ViewEditAction::class, [

            'method' => ['GET'],

            'path' => 'view_edit/:id'

        ]);

    }

Configure actions using action class map.

Each action class uses $_actionsClassMap for defining a map between crud (and non crud) actions on the name of the action class.

Non crud actions should be additionally mapped, which is described in the previous step.

use App\Service\Protocols\IndexAction;

class ProtocolsService extends AppFallbackService

{

    /**

     * Actions classes map.

     *

     * @var array

     */

    protected $_actionsClassMap = [

        'index' => IndexAction::class,

    ];

Configure service and action in config file

Service options are defined in the config/api.php in Api.Service section.

Let's consider configuration options for ArticlesService.

Configuration are hierarchical in the next sense: 

  • define default options for any service within the application in the Api.Service.default.options section.
  • define options for any service within the application in Api.Service.articles.options section.

All defined options are overridden from up to down in described order.

This allows common service settings, and the ability to overwrite them in bottom level.

  •  Api.Service.classMap - defines name map, that allows defining services action classes with custom location logic.
    Any action, that could be loaded as default action defined in fallback class, or specific action class could be configured using configuration file.
    Let's consider how one can configure options for IndexAction of ArticlesService.
    Configuration are hierarchical in the next sense: 
  • one can define default options for any action for all services in the application in the Api.Service.default.Action.default section.
  • one can define default options for index action for all services in the application in the Api.Service.default.Action.index section.
  • one can define options for any action in the specific (articles) service in the Api.Service.articles.Action.default section.
  • one can define options for index action in the specific (articles) service in the  Api.Service.articles.Action.index section.

Crud and non crud methods. Mapping non-crud actions.

Crud services mapped automatically in two levels routing by FallbackService.

Index and view. Formatting output

The CakeDC Api Plugin is flexible and provides multiple ways to prepare result data for the response objects.

There is a list of main options.

Use Entity serialization

The most trivial way to convert data is using entity serialization.

When converting an entity to a JSON, the virtual and hidden field lists are applied. 

Entities are recursively converted to JSON as well. 

This means that if you eager, and loading entities and their associations, CakePHP will correctly handle converting the associated data into the correct format.

Additional fields could be defined using Entity::$_virtual and hidden using Entity::$$_hidden.

Build object manually from Action::execute

In this case users manually perform mapping of requests received from model layer to output array.

public function process()

{

    $entity = $this->getTable()->get($this->getId());

    return [

        'id' => $entity->id,

        'name' => $entity->name,

    ];

}

Use Query::formatResults in model layer

The request could be formatted in model layer using: Query::formatResults.

So in this case, the process action just calls for a needed finder from the model layer and returns the result.

public function findApiFormat(Query $query, array $options)

{

    return $query

        ->select(['id', 'body', 'created', 'modified', 'author_id'])

        ->formatResults(function ($results) use ($options) {

            return $results->map(function ($row) use ($options) {

                $row['author'] = $this->Authors->getFormatted($row['author_id']);

                unset($row['author_id']);

 

                return $row;

            });

        });

Use Action extensions to format output

In index action defined callback Action.Crud.afterFindEntities, which called after data fetched,  could be used to extend or overload results coming from the database.

Callbacks are catch-in-action extensions and could be applied to multiple endpoints.

For view action defined Action.Crud.afterFindEntity, which called after single record fetched.

Use Action extensions to append additional data to output

Sometimes there is some additional information needed to be presented in some group of endpoints. In this case it is possible to implement an action extension to append additional data.

For example, pagination provides information about number of pages, records count, and current page number.

Another example for additional data is some execution statistics about the query.

Here you see main parts of appending such data from extension.

class PaginateExtension extends Extension implements EventListenerInterface

{

    public function implementedEvents(): array

    {

        return [

            'Action.Crud.afterFindEntities' => 'afterFind',

        ];

    }

...

    public function afterFind(EventInterface $event): void

    {

        ...

        $pagination = [

            'page' => $this->_page($action),

            'limit' => $limit,

            'pages' => ceil($count / $limit),

            'count' => $count,

        ];

        $result->appendPayload('pagination', $pagination);

    }  

 

The renderer class describes how to handle payload data.

For example in JSend renderer, all payload records appended to the root of the resulting json object.

Rendering output. Renderers.

Renderers perform final mapping of response records to output format. 

Such formats like xml, json, or file are provided by  The CakeDC API plugin.

JSend is the json extension with some additional agreements about returning results.

 

 

Latest articles

Why Use CakePHP

CakePHP  is a web development framework running on PHP. CakePHP provides tools to help build websites and web apps faster, stable and very easy to maintain. We will outline some of the interesting features of the CakePHP framework below:

Authorization

The authorization layer is in charge of determining if a user is able to access a feature or not. Authorization in CakePHP may be as complex as you want. It is powerful because you can define permissions per role, ownership, or anything else by just writing a policy.  In CakePHP 4, the Authorization layer is part of another package, which means that it can be used in a non-CakePHP app.

MVC Support

The layers in CakePHP are very explicit. Firstly, you will see that the application has specific folders for each layer (Model, Controller, Template). Secondly, you are encouraged to not access layers incorrectly, because the right way is simple enough. After using multiple frameworks out of there, I can say that CakePHP implements MVC and it implements it well.

Bake

Bake is not something new in CakePHP 4. It has been included with Cake since version 0.1.0 and even now when it is released as a plugin, it is a required tool for any developer. Bake makes generating an application, controllers, models and everything else easier... just running a command and within minutes. Other frameworks may have some tools, but in my opinion, there is nothing like bake.

Database Migration

Database migrations with CakePHP are simple, quick and safe. Those are probably the only three things you look for when versioning your database. MigrateShell can be used to generate migrations and you are able to alter database structure in any way, as well as running queries/commands. The CakePHP team is also responsible for Phinx plugin development.

Multi Language Support

This is another old feature that has been in CakePHP for years and has been recently improved in CakePHP 4. I must say that we have not found any other framework with the same set of features for internationalization and localization. You only need to use the translation function for any strings [__()] and define a way to set the language in your app. After doing that, just extract PO files using the shell and start translating your strings.

Powerful ORM

ORM in CakePHP 4 is able to do anything you can imagine. Simple and complex queries have an easy way to be executed using ORM functions. Joins, grouping, subqueries can be done in just minutes. Even after working in complex reporting for several clients, we have not found something that cannot be done through CakePHP ORM.

PHP Standards 

Going back to previous CakePHP versions (1.x and <2.x) we found that they implemented their own standard, which was good if you only worked with CakePHP... but it made integrating external libraries more difficult. In CakePHP 4, as well as 3.x, it is not a problem anymore because the wonderful CakePHP team is aware of the latest standards for the language and they are implemented inside the core... as well as the libraries released around it. It allows you to use any PHP library or package inside your Cake application.  I could go on all day about features that I like about the CakePHP framework, but there's more blogs to write in the future. I hope this gives you some incentive to use it.  

Managing A Company During A Global Pandemic

A worldwide pandemic is not something a company, a manager, or a team ever plans for. This time 6 months ago we were bringing in more clients than anticipated, and planning for an international conference. Fast forward to now, just like most companies we have been hit, our conference has gone virtual, and many employees are still worried about what is to come.  Here are 5 things I have learned during these uncertain times:  

1. Don’t panic. 

Easier said than done, right? Being responsible for a team of great people and their financial, as well as professional, well being can be trying at times. I have learned it is best to stay calm, push forward and still do the best we can, even when our best isn’t always enough. Luckily, I am not a worrier by nature, and I hope that I can be a solid backbone for my team and clients, while letting them know that they (and our company) are my top priority … now more than ever.   

2. Be transparent

It is best to have sure answers and knowledge of what is expected, and to be open and honest about this with the team. If we are going to be working longer hours one week, maybe shorter the next, I want to be upfront so that no one is caught off guard. If policies or procedures are changing, they are notified immediately   Same thing goes for our clients, we have always prided ourselves on being honest and transparent about the behind the scene scenarios. It may not always be good news that’s delivered, but it will be honest. I have set goals to make my expectations clear, and reasonable.   

3. Be available:

Someone like myself, I am always going in a million directions. I have made it a point to make myself available for help, support, or whatever it is that someone may need. This goes for  clients, team members, even friends.  A pandemic like this really makes you step back and think about what is important, and things that you may not have made priorities in the past that needed to change. Our team is used to working remotely, but we communicate daily, and we always have open lines of communication (sometimes in the middle of the night as we all work in different time zones.  

4. Be Understanding

If 2020 has taught us anything, it’s to expect the unexpected. It is vital that each individual understands that not everyone is the same. We do not handle stress the same, we go through trials and tribulations differently, so it is important to be empathetic. We need to provide tools for success... and sometimes that means paid time off, new communication platforms, team building exercises, or just listening and being compassionate. I think a mistake a lot of people made early on was expecting everyone to adjust to the new way of life, with no clear direction. This resulted in a lot of confusion, and negativity, instead of learning together and changing the course of action. 
 

5. Surround yourself with a great team. 

Luckily for me, our team is fully functional without me, maybe that should scare me a little, huh? We have structure, we built up trust with each other, and everyone works towards the same goal - being successful, delivering to our clients, and growing together. While it may be my job to keep everyone moving forward and be motivating, it’s no secret that they motivate me, too. Despite working in different countries, our team has built great relationships with each other, and everyone is ready to step in and help their colleagues whenever necessary. 
 

Final thoughts

Has 2020 been different than I imagined? Absolutely. We do not know the answers to every question. We also do not know where this year may take our team, the company, or the world! One thing I do know is we will adapt, adjust and keep pushing forward. We will keep providing the best service to our clients as we always have, and we will not panic…. Not yet at least. 
 

A Quick CakePHP Local Environment With Docker

CakePHP and Docker

We all know that while developing a CakePHP software, we need to have a local environment with PHP, HTTP Server (nginx, apache) and a database (MySql, Postgres, Mongodb, etc). Installing those tools directly to your system is the basic way, but it can become a bit tricky when we have multiple projects using different versions of those tools... that’s where Docker will help us. In this article, we will show a quick docker setup to improve our CakePHP local environment. If you don’t have docker installed go to: https://docs.docker.com/get-docker/. It is available for Linux, MacOS and Windows. For our setup we are going to use PHP, Nginx, and Mysql. All of the information required will be added to a new file named docker-compose.yml. In our environment we will need two docker images [https://docs.docker.com/engine/reference/commandline/images/], one image for php + nginx and one for mysql.   

Setup Nginx + PHP service

Create the file  docker-compose.yml inside your application with this:    version: "3.1" services:   php-fpm:     image: webdevops/php-nginx:7.4     container_name: myapp-webserver     working_dir: /app     volumes:       - ./:/app     environment:       - WEB_DOCUMENT_ROOT=/app/webroot     ports:       - "80:80"   Now,we have a service named php-fpm, which is able to run php 7.4 and nginx at port 80 pointing to our webroot dir. Important note: the container_name must be unique in your system.   

Setup MySql service

Our MySql service requires a username, password and database name. For this, we are going to create the file mysql.env (don’t use a weak password in production, you could share a mysql.env.default file with your team) with this content:   MYSQL_ROOT_PASSWORD=password MYSQL_DATABASE=my_app MYSQL_USER=my_user MYSQL_PASSWORD=password   Now, at the end of docker-compose.yml , add this:      mysql:     image: mysql:5.6     container_name: myapp-mysql     working_dir: /app     volumes:       - .:/app       - ./tmp/data/mysql_db:/var/lib/mysql     env_file:       - mysql.env     command: mysqld --character-set-server=utf8 --init-connect='SET NAMES UTF8;'     ports:       - "3306:3306"   Before we start this service, lets add the service for our database, include this at the end of the file:  docker-compose.yml . You’ll see that we have - ./tmp/data/mysql_db:/var/lib/mysql, this allows us to persist mysql data. Now we also have a service named mysql with one empty database named my_app and a user name my_user.
 

Starting the services and app configuration

Before we continue, make sure that you don’t have any other http server or mysql server running. Now that we have finished our docker-compose.yml  we can execute docker-compose up to start the services and access the app at http://localhost. The next thing you need to do is update your database configuration with the correct credentials - the host is the service name, in our case it is “mysql”:   'host' => ‘mysql’,             'username' => 'my_user',             'password' => ‘password’,             'database' => 'my_app',   That’s it! Now we have a working local environment for our CakePHP app. We can now access the services using docker-compose exec php-fpm bash  and docker-compose exec mysql bash.  The files mentioned here (docker-compose.yml and mysql.env) can be found at  https://gist.github.com/CakeDCTeam/263a65336a85baab2667e08c907bfff6.  

The icing on the cake

Going one step further, we could add some alias (with linux) to make it even easier. Let’s add these lines at the end of your ~/.bashrc file:   alias cake="docker-compose exec -u $(id -u ${USER}):$(id -g ${USER}) php-fpm bin/cake" alias fpm="docker-compose exec -u $(id -u ${USER}):$(id -g ${USER}) php-fpm" alias composer="docker-compose exec -u $(id -u ${USER}):$(id -g ${USER}) php-fpm composer"   With those entries, instead of typing docker-compose exec php-fpm bin/cake, we can just type cake. The other two aliases are for composer and bash. Notice that we have ${USER}? This will ensure that we are using the same user inside the services.  

Additional information

Normally docker images allow us to customize the service, for webdevops/php-nginx:7.4 - you can check more information at: https://dockerfile.readthedocs.io/en/latest/content/DockerImages/dockerfiles/php-nginx.html and for mysql check: https://hub.docker.com/_/mysql . You can find more images at: https://hub.docker.com/. If you are not familiar with docker, take a look at: https://docs.docker.com/get-started/overview/, as this documentation provides good information.   Hope you have enjoyed this article and will take advantage of docker while working in your CakePHP application.  

We Bake with CakePHP