As you know, CakePHP announced the version 4.x last December.I recommend that you consider upgrading your applications to the next version, to keep up to date and get all the benefits. Now, let's see how to bake!
TIPS, INSIGHTS AND THE LATEST FROM THE EXPERTS BEHIND CAKEPHP
Written by Yevgeny on September 07, 2020 •
8535 VIEWS
SQL language for different databases has some differences, which could cause problems after migrations between these databases. Here, we collected some hints, problems could appear during migration from MySQL to PostgreSQL. We focus on the CakePHP model layer and query generation layer.
$query->where([‘Author.id = Article.author_id’])
which works fine till we dont need field alias quotes. In the case of migration to postgres, we might want to enable autoQuotes. In this case, we can use $query->newExpr()->equalFields(‘Author.id
, Article.author_id’)
.
ILIKE
.The case of old style conditions where method arrays straight forward, you’d just go with ILIKE
instead of LIKE
. But what if we want to use ILIKE
in builder methods... Here is that example:
return $query->where(
function (QueryExpression $exp) use ($field, $value): QueryExpression {
return $exp->add(new \Cake\Database\Expression\Comparison($field, $value, 'string', 'ILIKE'));
});
FunctionExpression
: $expr = (new FunctionExpression('CAST'))->setConjunction(' AS ')->add([$id, 'varchar' => 'literal']);
which generates expression like :id AS varchar
there :id is the placeholder for variable $id. This trick, used with literal, allows you to cast to any postgres type.
IS NULL
as string. So if array syntax is used, all CakePHP conventions must be followed. However, sometimes we should help the ORM and obviously wrap a field name with IdentifiedExpression
. Let's take a look back to the previous example, but now we want to type cast not value, but a field. The only solution to quote field name correctly is using this code:
$id = new IdentifierExpression($this->aliasField($field));
$expr = (new FunctionExpression('CAST'))->setConjunction(' AS ')->add([$id, 'varchar' => 'literal']);
“Events”.”time_to” - “Events”.”time_from”
...
With ListExpression, it can be done quite easy:
$diff = new ListExpression([new IdentifierExpression('Events.time_to'), '-', new IdentifierExpression('Events.time_from')]);
Hopefully these tricks will be as useful for your baking as they have been for mine!
Written by Yelitza on August 31, 2020 •
10146 VIEWS
The Cake Development Corporation team performs many code reviews. In fact, that is our starting point with every new client, as we offer free quick reviews. This is a good way to see where code stands, and how much work will need to be done to get it functioning properly.
One of the common errors we have found while doing Code Reviews of existing applications or just working with inherited code, it’s the way HasMany relations data is saved.
We have noticed that to save HasMany relations, some developers save the target relation, and then when the ID is retrieved after saving, they save one per one each item of the ‘many’ relation. There is no need to do this, as CakePHP can do all this in one single ‘save’! You won’t have any issue related to inconsistent data, because everything will be stored in one single transaction and your code will look much more clean.
Let’s see a quick and easy to follow example - We will have the following relations: ‘Users’ and one User could have many ‘Addresses’. We wish to save one user and this user will have two addresses.
First, you need to build the form in the proper way, the request data should follow the structure of your entities. The key in the form is the fieldName for the hasMany inputs. They must follow this format: {entityname}.{position}.{property}, for example: adddress.0.street_1
, adddress.0.street_2
, etc for the first item so store, for the second one: : adddress.1.street_1
, adddress.1.street_2
, and so on. More examples can be found here: https://book.cakephp.org/4/en/views/helpers/form.html#creating-inputs-for-associated-data.
<?= $this->Form->create($user) ?>
<fieldset>
<legend><?= __('Add User') ?></legend>
<?php
echo $this->Form->control('first_name');
echo $this->Form->control('last_name');
echo $this->Form->control('phone');
?>
<legend><?= __('Address 1') ?></legend>
<?php
echo $this->Form->control('addresses.0.street_1');
echo $this->Form->control('addresses.0.street_2');
echo $this->Form->control('addresses.0.zip');
echo $this->Form->control('addresses.0.city');
echo $this->Form->control('addresses.0.state');
?>
<legend><?= __('Address 2') ?></legend>
<?php
echo $this->Form->control('addresses.1.street_1');
echo $this->Form->control('addresses.1.street_2');
echo $this->Form->control('addresses.1.zip');
echo $this->Form->control('addresses.1.city');
echo $this->Form->control('addresses.1.state');
?>
</fieldset>
<?= $this->Form->button(__('Submit')) ?>
<?= $this->Form->end() ?>
Now that we have the form, we need to convert the request data. The Table class provides an easy and efficient way to convert one or many entities from request data. It’s needed to define which associations should be marshalled, using associated
.
public function add()
{
$user = $this->Users->newEmptyEntity();
if ($this->request->is('post')) {
$user = $this->Users->patchEntity($user, $this->request->getData(), ['associated' => ['Addresses']]);
if ($this->Users->save($user)) {
$this->Flash->success(__('The user has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The user could not be saved. Please, try again.'));
}
$this->set(compact('user'));
}
In this example we are saving one user and two addresses for the given user.
Associated data is validated by default, If you wish to bypass data validation, pass the validate => false
option, for example: $this->Users->patchEntity($user, $this->request->getData(), ['associated' => ['Addresses' => [‘validate’ => false]]])
.
We are all about working smarter and in less time, so I hope this information will be useful! Take a look here for more information: https://book.cakephp.org/4/en/orm/saving-data.html#converting-request-data-into-entities
We will be posting more Common Errors while using CakePHP. Keep checking back!
Written by Amanda on August 17, 2020 •
7522 VIEWS
There we were… just 8 months ago, bright eyed and planning our annual CakeFest event for 2020. The community picked Los Angeles for a host city, and who doesn’t love LA? Our team was excited! Now, fast forward, and we are planning our virtual event. After sitting in on a few well executed virtual events this year, I was pleasantly surprised and impressed. There are still ways to keep these conferences interactive, but theres no denying that it is hard to beat the face to face communication within our CakePHP community, like we have experienced in the past. Wondering what our (and many others) event will look like this year? Let’s see….
Written by Ajibarra on August 10, 2020 •
7303 VIEWS
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:
Written by Larry on July 27, 2020 •
6791 VIEWS
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:
Written by Rochamarcelo on July 20, 2020 •
25048 VIEWS
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.
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.
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.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.
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.
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.
Written by Jorge on July 13, 2020 •
12249 VIEWS
Some time ago, we established https://git.cakedc.com/ as our company workflow. Along with it we created automated tools to support a continuous integration environment, with automated deployments based on develop
, qa
, stage
, master
branches and some useful tools to run on specific branches. We used jenkins for a long time, then switched to gitlab around version 6 (more than 5 years ago!) and we've been using it since.
Gitlab provides a very powerful way to configure your pipelines and define specific docker images to be used as your runners. So we defined our own runner image and configured it to provide the typical dependencies needed to run static analysis tools, unit tests and other utilities as part of our build process. For example, one typical build file for a simple CakePHP project could be:
# https://hub.docker.com/r/jorgegonzalezcakedc/cakephp-runner
image: jorgegonzalezcakedc/cakephp-runner:yarn
before_script:
# install ssh-agent
- 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'
# run ssh-agent
- eval $(ssh-agent -s)
# add ssh key stored in SSH_PRIVATE_KEY variable to the agent store
- ssh-add <(echo "$SSH_CI_PRIVATE_KEY")
- echo "$SSH_CI_PRIVATE_KEY" > /root/.ssh/id_rsa
- chmod 600 /root/.ssh/id_rsa
# replace git oauth key for composer
- sed -i "s/__TOKEN__/$GIT_OAUTH_TOKEN/g" ~/.composer/auth.json
variables:
# Configure mysql service (https://hub.docker.com/_/mysql/)
MYSQL_DATABASE: app_test
MYSQL_ROOT_PASSWORD: root
stages:
- test
- deploy
test:mysql:
services:
- mysql:5.7.22
script:
- echo $MYSQL_PORT_3306_TCP_ADDR
- composer install --verbose --prefer-dist --optimize-autoloader --no-progress --no-interaction
- ( [ -f vendor/bin/phpunit ] && echo "phpunit already installed";) || composer require phpunit/phpunit
- mysql -uroot -p$MYSQL_ROOT_PASSWORD -h $MYSQL_PORT_3306_TCP_ADDR -e 'CREATE DATABASE test_myapp_template;';
- DATABASE_TEST_TEMPLATE_URL="mysql://root:$MYSQL_ROOT_PASSWORD@$MYSQL_PORT_3306_TCP_ADDR/test_myapp_template" bin/cake db_test -i
- DATABASE_TEST_URL="mysql://root:$MYSQL_ROOT_PASSWORD@$MYSQL_PORT_3306_TCP_ADDR/app_test" DATABASE_TEST_TEMPLATE_URL="mysql://root:$MYSQL_ROOT_PASSWORD@$MYSQL_PORT_3306_TCP_ADDR/test_myapp_template" QUEUE_DEFAULT_URL='null:///?queue=default&timeout=1' vendor/bin/phpunit --verbose --colors=never
except:
- tags
deploy_develop:
stage: deploy
environment:
name: develop
url: https://www.cakedc.com
script:
- cd deploy && php deployer.phar deploy develop -vvv
only:
- develop
except:
- tags
In this case, on every push to the "develop" branch, we'll run unit tests of the project, then call the specific deploy script to push the project to our CI environment.
This process is usually smooth and clean, if it's not, then you need to debug why the runner is failing at some step.
One possible answer to this situation would be to dockerize the project and ensure the local docker version matches 100% the runner being used, so you don't have surprises while running your pipelines. This process is actually done in some projects to ensure we match specific versions and dependencies. But for legacy projects, it's useful to have something more or less generic that just works™ and does not require the effort to dockerize. In this case, and going back to the topic of the article, how can we debug the issues locally without waiting for the pipelines to run? (Note I'm using Ubuntu 16.04 as my dev environment, and possibly aiming to switch to 20.04 LTS soon…)
sudo apt update
&& sudo apt install -y gitlab-runner
--env
and you can name the target you want to build, in this case test:mysql
gitlab-runner exec docker test:mysql --env SSH_CI_PRIVATE_KEY="`cat ~/.ssh/id_rsa`" --env GIT_OAUTH_TOKEN="XXX"
sleep 1337
Written by Rafael on July 06, 2020 •
9420 VIEWS
Last december, the CakePHP team announced the immediate availability of 4.0.0. This release begins a new chapter for CakePHP, as 4.0 is now API stable. With this release, Cake 3.x moves into maintenance mode, while 2.x moves into security release mode. The promise of the version is: cleaner, faster and still tasty as usual. I had the opportunity to bake a new application from scratch and I will give my feedback about my process.
src/Locale/pt_BR/default.po (3.x)
resources/locales/pt_BR/default.po (4.x)
$product = $this->Products->newEmptyEntity();
{
public function initialize()
{
parent::initialize();
$this->loadComponent('Security', [
'blackHoleCallback' => 'forceSSL',
]);
}
public function beforeFilter(Event $event)
{
if (!Configure::read('debug')) {
$this->Security->requireSecure();
}
}
public function forceSSL()
{
return $this->redirect(
'https://' .
env('SERVER_NAME') .
Router::url($this->request->getRequestTarget())
);
}
}
public function middleware(MiddlewareQueue $middlewareQueue)
{
$middlewareQueue
->add(new HttpsEnforcerMiddleware([
'redirect' => true,
'statusCode' => 302,
'disableOnDebug' => true,
]));
return $middlewareQueue;
}
What I know is a drop, what I don’t know is an ocean. The new version is here to stay, and this article it's a just one overview of basic usage of the new version.
* Version 4.1.0 is released already with more improvements and features.
Written by Yevgeny on June 29, 2020 •
14024 VIEWS
This article covers new changes for CakePHP 4 version of plugin. So it covers versions starting from 8.x (8.0) and later.
return [
'CakeDC/Auth.api_permissions' => [
[
'role' => '*',
'service' => '*',
'action' => '*',
'method' => '*',
'bypassAuth' => true,
],
],
];
Now, consider the case we want to use users plugin authentication.
Since Api is supposed to be used from another domain, we should allow all requests with OPTIONS type.
To do this we should add this rule as first on in config/api_permissions.php
[
'role' => '*',
'service' => '*',
'action' => '*',
'method' => 'OPTIONS',
'bypassAuth' => true,
],
Here, method define OPTIONS and bypassAuth means that such actions should work for any users, including not authenticated.
Now we should allow Auth service methods
[
'role' => '*',
'service' => '*',
'action' => ['login', 'jwt_login', 'register', 'jwt_refresh',],
'method' => ['POST'],
'bypassAuth' => true,
],
All other services/actions should be declared in api_permissions file to define what user roles are allowed to access them.
Imagine we want to allow the admin
role to access the add/edit/delete posts and make index and view public.
We can do it based on method or based on action names.
[
'role' => 'admin',
'service' => 'posts',
'action' => '*',
'method' => ['POST', 'PUT', 'DELETE'],
],
[
'role' => 'admin',
'service' => 'posts',
'action' => ['index', 'view'],
'method' => '*',
'bypassAuth' => true,
],
'Middleware' => [
'authentication' => [
'class' => AuthenticationMiddleware::class,
'request' => ApiInitializer::class,
'method' => 'getAuthenticationService',
],
'bodyParser' => [
'class' => BodyParserMiddleware::class,
],
'apiParser' => [
'class' => ParseApiRequestMiddleware::class,
],
'apiAuthorize' => [
'class' => AuthorizationMiddleware::class,
'request' => ApiInitializer::class,
'params' => [
'unauthorizedHandler' => 'CakeDC/Api.ApiException',
],
],
'apiAuthorizeRequest' => [
'class' => RequestAuthorizationMiddleware::class,
],
'apiProcessor' => [
'class' => ProcessApiRequestMiddleware::class,
],
],
First we see the order of middlewares that proceed api request.
It passes through AuthenticationMiddleware, AuthorizationMiddleware, and RequestAuthorizationMiddleware to perform generic auth tasks.
It passes through BodyParserMiddleware to unpack the json request.
And finally ParseApiRequestMiddleware does initial service analysis and ProcessApiRequestMiddleware performs the request.
Also we can note CakeDC\Api\ApiInitializer class used to define Authentication
and Authorization
configuration. It can be redefined in the application layer to provide needed Identifiers
and Authenticators
.
access_token
and refresh_token
included into the login response.
Tokens should be passed in the Authorization header with bearer
prefix.
Access token is supposed to be used as default token and refresh token needed to get a new access token when it's expired. So for refreshing provided additional jwt_refresh
action which should be used in this case.
Configure::write('Api.config', ['api']);
'Api' => [
...
'Jwt' => [
'enabled' => true,
'AccessToken' => [
'lifetime' => 600,
'secret' => 'accesssecret',
],
'RefreshToken' => [
'lifetime' => 2 * WEEK,
'secret' => 'refreshsecret',
],
],
Hopefully, this was helpful. Our team is always working on adding new features and plugins. You can check out more available plugins HERE.
Written by Yelitza on June 22, 2020 •
8110 VIEWS
Developers are used to living in a virtual world, so adjusting has been easier than expected. Recently, we’ve been holding virtual meetups, and we are so happy with the feedback. Digital training sessions allow bakers from all over the world to come together and enjoy. Our plan is to host one each month, and coordinate time zones so that everyone gets a chance to attend. Our latest one was based around a good time for our Japanese community. If you missed the meetup, no problem. We always post the recording for playback, and I’ll even give you a quick rundown of the topics covered. Let’s jump in:
Written by Ajibarra on June 15, 2020 •
7772 VIEWS
Long before COVID-19, I (and my team) started working from home. Even before working at CakeDC, between 2007 and 2010, I worked for a company where you were able to split your time between home and office.
Maybe that's why I actually feel comfortable working remotely and I would never change it for an in office position anymore.
Anyway, I am not going to talk about remote work because these days there are thousands of articles about it (even in one of our previous posts). I want to focus today on writing about our experience with different communication tools and the pros and cons for each of them.
Back in 2011 when I started working at CakeDC, we had an IRC server in place with a couple of plugins to retain messages for our day-to-day communication. Everyone used different clients like Colloquy on mac or Pidgin on Linux. Additionally, we used Skype for peer/team calls and also for client communication. In my opinion, the linux experience was awful until they improved the client a few years later. This setup was implemented in 2007 when the company was started, and in 2012, we decided to shut it down because it was easier just using Skype for everything, messages and calls.
After several years using - suffering - Skype, with new options in the market, we decided to move away to a more reliable and modern approach. The main reason was the lack of updates for Skype linux client, and the message retention limits. In 2016 we started utilizing the more than popular Slack and its open source alternative, Rocket Chat; always keeping Skype for client communication.
Some months later the team concluded that Rocket Chat was the right choice, mainly because we wanted to have control over the messages and the information transmitted. Since the features we were using were available in both solutions, we installed our own Rocket Chat server. At this point I have to say that we did try the calls solution (Jitsy) inside Rocket Chat, but the experience was not good at all, issues with missing calls, poor call quality, etc, made us keep Skype for calls.
On the other hand CakePHP training was provided using Join.me and even when it worked very well in most situations; our trainer Jorge, always had to use a Windows machine instead of his usual Linux one. And then, Zoom emerged.
The year was 2018, when Zoom became so popular even though it started back in 2011 (yes, Zoom existed before COVID-19 crisis). We started using it and it quickly replaced Skype for our team calls. It allowed multiple people calls, screen share, etc. I must say, however, the Zoom chat is one of the worst things I have ever seen.
Going back to Jorge, as you can imagine he was very happy when he saw Zoom had a good Linux client. Unfortunately, he was quickly disappointed because the client screen crashed randomly, went black, and the computer ran out of memory when he tried to share his screen to more than 10 attendees. Happily he didn't throw his windows machine out of the window yet so he could continue giving the training with that machine.
It's 2020, COVID-19 is around us, and Zoom is probably the most popular telecommuting tool. For both adults and children, it is an essential tool to keep working and studying. However, fame never comes alone and threats, rumors and comments are making us move away (again) from Zoom to Google Meet. Also, it didn't make sense to pay for Zoom if we were already paying for GSuite.
I didn't mention it before, but we have been using GSuite (former Google Apps) since the beginning. Google - late as usual - detected a market niche and decided to pump out its meeting tool. In my opinion, I am a standalone app person. This means that I will always prefer having 20 apps instead of 20 browser tabs. So, I don't like Google Meet a lot because of this, but I must say the call quality is superb.
I am not sure how or when we will move to other tools, but right now we are very happy with our Rocket Chat installation and we are still getting used to Google Meet, but it fits our needs.
As a side note we are still using skype to communicate with clients because it is the app everyone has, and sometimes people simply don't want to install something else or use something they are not used to.
To conclude I must say that each team and person should try all of these different tools before choosing one, because one tool that may fits my needs may not fit yours.
Written by Amanda on June 01, 2020 •
8054 VIEWS
Unless you’ve been on Mars for the past few months, you know that things have changed. We can no longer prance worry-free into a department store, or go grab drinks with friends. Everything is now masks, grocery delivery, and video chats. While this was a hard transition at first, we have surprisingly become well-adjusted.
Written by Rochamarcelo on May 25, 2020 •
17935 VIEWS
In Software Development, we are always looking for resources to improve the products we deliver. In this article we show some awesome php qa tools to help us improve our CakePHP software quality and development process. Let's dive in.
Note: The snippets provided in this article can be found at https://github.com/CakeDC/phpqa-app-example
composer require --dev cakephp/cakephp-codesniffer:~4.1.0
<?xml version="1.0"?>
<ruleset name="App">
<config name="installed_paths" value="../../cakephp/cakephp-codesniffer"/>
<rule ref="CakePHP"/>
</ruleset>
"scripts": {
"cs-check": "phpcs -p ./src ./tests",
"cs-fix": "phpcbf -p ./src ./tests",
composer cs-check
and composer cs-fix
. In a sample app I got this output saying some errors can be autofixed with composer cs-fixcomposer require --dev phpstan/phpstan phpstan/extension-installer cakedc/cakephp-phpstan:^1.0
Includes:
- vendor/cakedc/cakephp-phpstan/extension.neon
- phpstan-baseline.neon
parameters:
level: 6
checkMissingIterableValueType: false
checkGenericClassInNonGenericObjectType: false
autoload_files:
- tests/bootstrap.php
ignoreErrors:
services:
parameters:
ignoreErrors:
"stan": "phpstan analyse src/",
"stan-rebuild-baseline": "phpstan analyse --configuration phpstan.neon --error-format baselineNeon src/ > phpstan-baseline.neon",
composer stan
and composer stan-rebuild-baseline
the second one will populate phpstan-baseline.neon to ignore all errors returned in composer stan so only use when all the errors shown should be ignored.
composer require --dev "psalm/phar:~3.11.2"
<?xml version="1.0"?>
<psalm
allowCoercionFromStringToClassConst="true"
allowStringToStandInForClass="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
errorBaseline="psalm-baseline.xml"
>
<projectFiles>
<directory name="src" />
</projectFiles>
<issueHandlers>
<LessSpecificReturnType errorLevel="info" />
<RedundantConditionGivenDocblockType errorLevel="info" />
<TypeCoercion errorLevel="info" />
<DocblockTypeContradiction errorLevel="info" />
</issueHandlers>
</psalm>
<?xml version="1.0" encoding="UTF-8"?>
<files psalm-version="3.11.2@d470903722cfcbc1cd04744c5491d3e6d13ec3d9">
</files>
"psalm": "psalm.phar --show-info=false src/ ",
"psalm-rebuild-baseline": "psalm.phar --show-info=false --set-baseline=psalm-baseline.xml src/"
composer psalm
and composer psalm-rebuild-baseline
the second one will populate psalm-baseline.xml to ignore all errors returned in composer stan, so only use this when all the errors shown should be ignored.
When we run composer psalm
it may inform you that some errors can be autofixed and which arguments you should use to fix. Normally it will be something like
vendor/bin/psalm.phar --alter --issues=MissingReturnType,MissingClosureReturnType --dry-run
--dry-run
part.
Written by Jorge on May 19, 2020 •
16852 VIEWS
As you may know, there are 2 new plugins "recently" (not so recently) added to deal with the concepts of Authentication and Authorization in your CakePHP applications.
Over the years, both Authentication and Authorization were managed in the Controller layer, via AuthComponent. These 2 things usually grow in complexity
as your project grows too, making the AuthComponent a complex class dealing with many features at the same time.
One of the original ideas behind these new plugins was to refactor AuthComponent and create specific layers to handle:
/tournaments/add
any user can create a new Tournament/tournaments/index
browse all joined tournaments/tournaments/invite
only current Members can invite others, and only if the Tournament has not started yetunauthorizedHandler
config will allow you to define what to do if the request was not authorized for some reason.AuthorizationComponent
, either to
skipAuthorization
when you don't require any specific condition to authorize the operation. Example:
// ... somewhere in your beforeFilter...
if ($user->is_superadmin) {
$this->Authentication->skipAuthorization();
}
// ...
authorize($resource, $action)
when you need to check if a given user is allowed to do some action on a given resource. Note the resource must be an Object.canInvite(IdentityInterface $user, Tournament $tournament)
$authorizationService = new AuthorizationService(new OrmResolver());
...
->add(new AuthorizationMiddleware($authorizationService, [
'unauthorizedHandler' => [
'className' => 'Authorization.Redirect',
'url' => '/users/login',
'queryParam' => 'redirectUrl',
],
]));
Note the $authorizationService
is configured with one resolver to match the CakePHP typical ORM classes, like Entities or Queries. https://book.cakephp.org/authorization/2/en/policy-resolvers.html#using-ormresolver
Once the middleware is added, you'll need to ensure the Authorization is checked, or you'll get an error?: "The request to / did not apply any authorization checks" .
The first step would be to skip authorization for all the controllers and actions, for example in beforeFilter
callback that all Users are allowed to access.
About the previous Tournaments specific cases, we'll need to create a new Policy class including all the possible actions to be done, for example:/tournaments/add
class TournamentPolicy
{
public function canAdd(IdentityInterface $user, Tournament $tournament)
{
// all users can create tournaments
return true;
}
}
file src/Controller/TournamentsController.php
// ...
public function add()
{
$tournament = $this->Tournaments->newEmptyEntity();
$this->Authorization->authorize($tournament);
if ($this->request->is('post')) {
// ...
The call to $this->Authorization->authorize($tournament)
; will map the Tournament entity to the TournamentPolicy, by default the action is taken from the controller action, in this case "add" so we will need to define a canAdd() method. We allowed all Users to create Tournaments.
/tournaments/index
class TournamentsTablePolicy
{
public function canIndex(IdentityInterface $user, Query $query)
{
// all users can browse tournaments
return true;
}
public function scopeIndex(IdentityInterface $user, Query $query)
{
// scope to filter tournaments for a logged in user
return $query->matching('TournamentMemberships', function (Query $q) use ($user) {
return $q->where(['TournamentMemberships.user_id' => $user->get('id')]);
});
}
}
file src/Controller/TournamentsController.php
public function index()
{
$query = $this->Tournaments->find();
$this->Authorization->authorize($query);
$tournaments = $this->paginate($this->Authorization->applyScope($query));
$this->set(compact('tournaments'));
}
/tournaments/invite
// ...
public function canInvite(IdentityInterface $user, Tournament $tournament)
{
return TableRegistry::getTableLocator()->get('TournamentMemberships')
->exists([
'user_id' => $user->get('id'),
'tournament_id' => $tournament->get('id'),
]);
}
// ...
file src/Controller/TournamentsController.php
// ...
public function invite($tournamentId, $userId)
{
$tournament = $this->Tournaments->get($tournamentId);
$this->Authorization->authorize($tournament);
// ...
In this case, we need to check if the logged in User is already a member of the TournamentMemberships group, if so, we are allowed to invite another user.
As you can see, Authorization plugin will provide a flexible way to manage your application permissions. In the previous examples we've covered typical application use cases to handle permissions per resource and action. New classes and interfaces, like policies, resolvers and mappers will allow you to configure the Authorization and ensure all the resources in your application will provide the required permissions.
If you're looking for RBAC based on your controller actions, take a look at https://github.com/CakeDC/auth/blob/master/Docs/Documentation/Authorization.md
For additional tools and plugins, check https://github.com/FriendsOfCake/awesome-cakephp#authentication-and-authorization
Written by Amanda on May 04, 2020 •
7974 VIEWS
As a marketer that works with web developers daily, I know that content may not be top priority on their long list of to-do’s. However, for the success of a company, it is essential. If your team doesn’t have a designated marketing person or team, and you’re not a seasoned content creator, I have 3 letters for you to learn: S.E.O. So what is SEO? It stands for search engine optimization. Basically this represents guidelines for gaining traffic to your website (or one you’ve been hired to create), and doing it organically. What is organic traffic? This is the results people see without being targeted, no paid ads, no cold calling - just desired results because your company offers something that they are interested in. Today’s market is extremely competitive, so it is important to take every step in making sure that your webpage stands out and is easy to find. Think about how you find information daily… how to make fried chicken? Where to get your car fixed? Or even when a new movie is being released? You search for it, online, right? 9 times out of 10, you’re probably going to run a search for it on a site like Google, right? Then, most likely, you’re going to click on one of the first couple results that pop up on your screen, because they include keywords relevant to the search that you performed. This is an example of SEO. You search for a term that is relevant or appears on a company’s website, and Google recognizes that term/phrase and yields the webpage to you as a result. Thus, increasing traffic for the website, and a lot of times, without any cost to them. Utilizing this idea, or service, is essential for any marketing department. Actually, according to a recent survey done by HubSpot, about 64% of marketers actively invest time in search engine optimization. So if you're not, you're falling behind. Now that you have a basic understanding of what SEO is, we can talk about some of the benefits.
Written by Rafael on April 27, 2020 •
10793 VIEWS
The only way to go fast, is to go well, my Uncle Bob always said. Research has shown that development with TDD evolves 10% faster than work without TDD. [See here]
CakePHP comes with comprehensive testing support built-in with integration for PHPUnit. It also offers some additional features to make testing easier.
This article will cover how to write Unit Tests with CakePHP and using the CakeDC DbTest plugin.
First, let's bake a new project:
composer create-project --prefer-dist cakephp/app:4.*
Now, we need to think about a model so we can create it and test it. I guess everybody has written a Products model before, our model would looks like this:
Name (string)
Slug (string, unique)
Description (text)
Price (decimal)
bin/cake bake migration CreateProducts name:string slug:string:unique price:decimal[5,2] description:text created modified
Pay attention, for slug, It was created with a unique index. Meanwhile our goal will be to have urls like: /slug-of-product and this way, the slug needs to be unique.
bin/cake migrations migrate
At this point, our database is ready with the `products` table and we can start coding and writing the tests.
* Note: some points were abstracted, such as installation, project configuration, and shell commands, because that's not the goal of the article. You can find all information on these in the cookbook.
bin/cake bake all Products
public function testAdd(): void
{
$this->enableCsrfToken();
$this->enableRetainFlashMessages();
$this->post('products/add', [
'name' => 'iPhone 11',
'slug' => 'iphone-11',
'price' => 699,
'description' => 'Lorem ipsum dolor sit amet, aliquet feugiat.',
]);
$this->assertResponseSuccess();
$this->assertFlashMessage(__('The product has been saved.'));
$this->assertRedirect('products');
}
Let's write another test that tries to add a duplicated product. First, we need to update the fixture, then write the test:
tests/Fixture/ProductsFixture.php
public function init(): void
{
$this->records = [
[
'id' => 1,
'name' => 'iPhone SE',
'slug' => 'iphone-se',
'price' => 399,
'description' => 'Lorem ipsum dolor sit amet, aliquet feugiat.',
'created' => '2020-04-23 13:12:58',
'modified' => '2020-04-23 13:12:58',
],
];
parent::init();
}
tests/TestCase/Controller/ProductsControllerTest.php
public function testAddDuplicated(): void
{
$this->enableCsrfToken();
$this->enableRetainFlashMessages();
$this->post('products/add', [
'name' => 'iPhone SE',
'slug' => 'iphone-se',
'price' => 399,
'description' => 'Lorem ipsum dolor sit amet, aliquet feugiat.',
]);
$this->assertResponseSuccess();
$this->assertFlashMessage(__('The product could not be saved. Please, try again.'));
$this->assertNoRedirect();
}
With these tests, we know the work is complete when the acceptance criteria (the slug of product must be unique) of the tests is passed.composer require cakedc/cakephp-db-test:dev-2.next
bin/cake plugin load CakeDC/DbTest
'Datasources' => [
// ...
'test_template' => [
'className' => Connection::class,
'driver' => Mysql::class,
'persistent' => false,
'timezone' => 'UTC',
//'encoding' => 'utf8mb4',
'flags' => [],
'cacheMetadata' => true,
'quoteIdentifiers' => false,
'log' => false,
//'init' => ['SET GLOBAL innodb_stats_on_metadata = 0'],
],
// ...
// migrate the database for template
bin/cake migrations migrate -c test_template
// import fixtures
bin/cake fixture_import dump
// generate dump
/bin/cake db_test -i
Finally, we can see some advantages of CakeDC DbTest:
Written by Amanda on April 13, 2020 •
7834 VIEWS
What a strange unprecedented time we are living in, right? Thanks, COVID-19. CakeFest, just like many other conferences and events, has been up in the air. Our team spent weeks discussing the possibility of moving forward as planned, having backup plans, or ways out of contracts should the emergency continue. After negotiations with pending sponsors, venues, vendors, etc., we toyed with the idea of going virtual and following suit of many others. After discussions with the core team, it was decided that this was the way to go.
This virus infused world is uncharted territory for billions of people, but one thing remains the same among us all…. the safety and health of our clients, families, and teams is the number one concern and priority. I’m sure everyone has encountered those who don’t understand these types of decisions, but allow me to put some numbers into perspective:
Currently, there are 1,696,588 cases of COVID-19 worldwide, and 105,952 deaths have been reported (as of April 12, 2020 via World Health Organization). According to hopkinsmedicine.org: “The virus can spread between people interacting in close proximity—for example, speaking, coughing, or sneezing—even if those people are not exhibiting symptoms. In light of this evidence, CDC recommends wearing cloth face coverings in public settings where other social distancing measures are difficult to maintain (e.g., grocery stores and pharmacies) especially in areas of significant community-based transmission.”
So what I am reading is that someone who has it, may not even know they are carrying the virus, and can spread the germ by just being in the same area as a peer. This is even more frightening due to the fact that there’s no research to this new virus, therefore no cure or vaccine to combat its effects on those infected.
With the statistics and facts about this virus, we made the difficult decision to go digital for CakeFest 2020. We understand that our intended dates in October are still far away, but without knowing how regulations and orders will change from government officials, it is hard to plan such an important event. Hopefully after learning more about how we came to this decision, you will understand and support us. We were extremely excited to bring the Cake to Los Angeles, but not at the cost of anyone’s health. We also hope to still deliver the knowledge, information, and possibly some ElePHPants to our virtual attendees this year (while also keeping you safe). The good news is, we can host in LA next year! And look at it this way, you can watch and learn in your pajamas!
So what will CakeFest look like this year? We are still in the planning phases, and allowing for sponsors. After a lot of research we hope to find the best platform for our needs, and the best speakers to share the knowledge. Speaking of which, if you have tips or suggestions for hosting a virtual conference, we’d love to hear from you! You can email us. Tickets will be sold (for less cost, there's a silver lining) for full access to all videos - before and after the event. We will conduct the conference as normal, but the difference is, we won’t be face to face.
To keep up with the planning process or any changes, make sure you follow us on Twitter, or Facebook. You can always stay up to date at CakeFest.org, too.
Written by Megan on April 06, 2020 •
7460 VIEWS
We’ve previously covered tips, tricks and resources to improve your working at home productivity - today we chat about how to improve your mental health! There are currently more and more people switching over to remote working - perhaps your company has changed policies, or maybe you’ve been forced to work at home due to a stay-at-home order - whatever the case, mental health while working at home is vitally important. Some of us are made to work at home - working from an office, may not be your jam, and the peace and serenity that a home office offers completes the mood. However, there are some of us that enjoy the daily routine, the commute to the office and the morning catch up around the coffee machine! So have you been stuck lately feeling a little more down than usual? Here are some tips to increase your mental health during this time.
Written by Yevgeny on March 30, 2020 •
11908 VIEWS
composer require cakedc/cakephp-api
php
$this->addPlugin(\CakeDC\Users\Plugin::class);
$this->addPlugin(\CakeDC\Api\Plugin::class, ['bootstrap' => true, 'routes' => true]);
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.
{"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.
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'
]);
}
$_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,
];
}
ArticlesService
.
Configuration are hierarchical in the next sense:
Api.Service.default.options
section.Api.Service.articles.options
section.IndexAction
of ArticlesService
.Api.Service.default.Action.default
section.index
action for all services in the application in the Api.Service.default.Action.index
section.articles
) service in the Api.Service.articles.Action.default
section.index
action in the specific (articles
) service in the Api.Service.articles.Action.index
section.Entity::$_virtual
and hidden using Entity::$$_hidden.
public function process()
{
$entity = $this->getTable()->get($this->getId());
return [
'id' => $entity->id,
'name' => $entity->name,
];
}
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;
});
});
}
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.