CakeDC Blog

TIPS, INSIGHTS AND THE LATEST FROM THE EXPERTS BEHIND CAKEPHP

Ed Finkler - Founder, Open Sourcing Mental Illness

Do you know who Ed Finkler is or what OSMI does? If you are in the developer community, then it definitely is a name you should get to know.

Open Sourcing Mental Illness is a non-profit organization  dedicated to raising awareness, educating, and providing resources to support mental wellness in the tech and open source communities.

CakeDC and CakePHP has long supported and stood behind OSMI - Ed Finkler has been instrumental in making mental health a topic of discussion, and opening up lines of support for mental wellness in tech. Mental health and wellness are close to our hearts and we want to share with you OSMI and why you should support it.

Ed has been active in bringing forward a previously rarely discussed topic - mental health. Being an advocate of mental health awareness and using his own experiences as a developer, he has recently announced that he is now able to go full time into OSMI. This is really fantastic news and CakeDC stands 100% behind him. We caught us with him to find out more.

We love that you are now putting all your time into OSMI - but what was the Catalyst for your decision to focus full time into OSMI?
What we found is that we simply had to much to do, and not enough time to do it. Everyone at OSMI are volunteers, and it was becoming increasingly challenging to find the bandwidth for anyone to complete major tasks. We are ambitious, and our ambition far exceeded the time available. I couldn’t ask it of anyone else, but I could make a decision myself -- that I would step away from my CTO role at a tech startup and dedicate myself to OSMI full-time.
What is your favorite thing to do out of ‘office’ hours (Hobbies/activities etc)?
Generally I find myself watching movies or good TV shows, or playing video games (I’m deep in Mass Effect: Andromeda right now). I also write electronic music, which you can hear at deadagent.net.
Do you think that companies are becoming more receptive to your message and becoming more open about speaking about mental health?
Yes, I think so. Companies in general are gradually becoming more aware of the need to discuss mental health openly, the same way we discuss other serious public health issues, like cancer and heart disease. But there’s a long, long way to go, and we are just taking our first steps as an industry to deal with this in a healthy way.
Have you seen a marked difference in people opening up about their personal experiences?
I definitely have observed, over and over, that when someone takes that first step forward, others follow. Fear is the thing that keeps mental illness hidden, and fear is why so many suffer in silence. Seeing someone speak without fear about their own issues empowers the listener. They may not need to stand up on stage like I do, but I’ve had numerous people tell me that hearing someone speak openly was what allowed them to seek help and/or start speaking openly about the subject.
What would you say is the biggest misconception that you have encountered when speaking about and sharing your personal experiences?
I think the biggest misconception I encounter is companies believing that by simply offering some level of mental health care in medical coverage, they’ve done all they can. That would be fine if we treated mental disorders like we do cancer or heart disease or diabetes, but we don’t -- we are afraid to discuss it, and as a consequence, we don’t know what to look for, why it matters, and how to seek help. In the absence of consistent, positive affirmation that it’s a safe topic, our default is to be afraid to discuss it. That keeps people from seeking the help they need.
Biggest piece of advice that you would give someone battling with mental health issues
You are not alone. Lots of people are like you. There is no shame in what you deal with. You are stronger than you know.
You recently spoke about mental health breaks on the OSMI blog, how would someone know they are in need of one and how would you suggest for employees to bring this topic up with their employers?
I am leery of giving specific health advice, but in general I’d say this: listen to your mind and your body, and remember that your own health is far, far more important than any job. Plus, if you’re healthy, you’ll be able to do your job much better.
In the last 5 years, you have achieved incredible breakthroughs and achievements in bringing this to the fore - where do you see OSMI and mental illness awareness in the next 5 years?
Ultimately, those two things are intertwined. OSMI will continue to grow because so many of us suffer from this, and more and more of us are realizing that we aren’t alone. That we aren’t broken. That we aren’t without hope. OSMI is about giving hope to those that felt they had none. Giving compassion to those who are hardest on themselves.
It’s my sincere hope that OSMI will drive the awareness of mental health in the tech workplace and change what we choose to value in employers and employees. However we get there, I believe we will succeed.

As someone suffering and wanting to find out more or be involved, how do we reach out, what should we expect and where should we go?
There are lots of ways to help OSMI, and all you really need is a willingness to spend some of your time working with us. You should visit https://osmihelp.org and learn more about our work, and then email [email protected] to talk to us about volunteering.
As a business with employees in the tech industry, what should we do to make mental health more accessible
For each employer there’s a different answer, but there are some general things to keep in mind. The biggest one is that the well-being of your employees must be a top priority. It’s an easy thing to say, but if you truly value it, you’ll avoid doing what so many organizations do: rewarding overwork and unhealthy “loyalty.” Ping pong tables and bean bag chairs don’t make people healthier, and neither do free snacks and beer at the office. They’re short-term tricks to get people to come to you and maybe stay in the office longer, but they don’t encourage a healthy work/life balance. Too many developers think their work IS their life. That’s a mistake.
Long term, what works are reasonable work hours, easy access to mental and physical health care, and promoting healthy preventative habits. Employees who feel that their well-being is demonstrably valued will be more productive and stay with your organization longer.
I also strongly encourage everyone in a leadership position to take Mental Health First Aid <https://www.mentalhealthfirstaid.org>, a program that teaches the skills to respond to the signs of mental illness and substance use.
Quote to live by or key advice to follow every day
One time I was encouraged to do a six-word memoir, and this is what I came up with:
“By helping others, I save myself.”

Thanks to Ed! We absolutely loved catching up with him about OSMI, we hope that you take a moment to check out the links and find out more to get involved and continue this important conversation!

For more information, be sure to check out https://osmihelp.org/about/about-osmi

 

Latest articles

Pagination of multiple queries in CakePHP

Pagination of multiple queries in CakePHP

A less typical use case for pagination in an appication is the need to paginate multiples queries. In CakePHP you can achieve this with pagination scopes.

Users list

Lest use as an example a simple users list. // src/Controller/UsersController.php class UsersController extends AppController { protected array $paginate = [ 'limit' => 25, ]; public function index() { // Default model pagination $this->set('users', $this->paginate($this->Users)); } } // templates/Users/index.php <h2><?= __('Users list') ?>/h2> <table> <thead> <tr> <th><?= $this->Paginator->sort('name', __('Name')) ?></th> <th><?= $this->Paginator->sort('email', __('Email')) ?></th> <th><?= $this->Paginator->sort('active', __('Active')) ?></th> </tr> </thead> <tbody> <?php foreach ($users as $user): ?> <tr> <td><?= h($user->name) ?></td> <td><?= h($user->email) ?></td> <td><?= $user->active ? 'Yes' : 'No' ?></td> </tr> <?php endforeach; ?> </tbody> </table> <?= $this->Paginator->counter() ?> <?= $this->Paginator->prev('« Previous') ?> <?= $this->Paginator->numbers() ?> <?= $this->Paginator->next('Next »') ?>

Pagination of multiple queries

Now, we want to display two paginated tables, one with the active users and the other with the inactive ones. // src/Controller/UsersController.php class UsersController extends AppController { protected array $paginate = [ 'Users' => [ 'scope' => 'active_users', 'limit' => 25, ], 'InactiveUsers' => [ 'scope' => 'inactive_users', 'limit' => 10, ], ]; public function index() { $activeUsers = $this->paginate( $this->Users->find()->where(['active' => true]), [scope: 'active_users'] ); // Load an additional table object with the custom alias set in the paginate property $inactiveUsersTable = $this->fetchTable('InactiveUsers', [ 'className' => \App\Model\Table\UsersTable::class, 'table' => 'users', 'entityClass' => 'App\Model\Entity\User', ]); $inactiveUsers = $this->paginate( $inactiveUsersTable->find()->where(['active' => false]), [scope: 'inactive_users'] ); $this->set(compact('users', 'inactiveUsers')); } } // templates/Users/index.php <?php // call `setPaginated` first with the results to be displayed next, so the paginator use the correct scope for the links $this->Paginator->setPaginated($users); ?> <h2><?= __('Active Users') ?>/h2> <table> <thead> <tr> <th><?= $this->Paginator->sort('name', __('Name')) ?></th> <th><?= $this->Paginator->sort('email', __('Email')) ?></th> <th><?= $this->Paginator->sort('active', __('Active')) ?></th> </tr> </thead> <tbody> <?php foreach ($users as $user): ?> <tr> <td><?= h($user->name) ?></td> <td><?= h($user->email) ?></td> <td><?= $user->active ? 'Yes' : 'No' ?></td> </tr> <?php endforeach; ?> </tbody> </table> <?= $this->Paginator->counter() ?> <?= $this->Paginator->prev('« Previous') ?> <?= $this->Paginator->numbers() ?> <?= $this->Paginator->next('Next »') ?> <?php // call `setPaginated` first with the results to be displayed next, so the paginator use the correct scope for the links $this->Paginator->setPaginated($inactiveUsers); ?> <h2><?= __('Inactive Users') ?>/h2> <table> <thead> <tr> <th><?= $this->Paginator->sort('name', __('Name')) ?></th> <th><?= $this->Paginator->sort('email', __('Email')) ?></th> <th><?= $this->Paginator->sort('active', __('Active')) ?></th> </tr> </thead> <tbody> <?php foreach ($inactiveUsers as $inactiveUser): ?> <tr> <td><?= h($inactiveUser->name) ?></td> <td><?= h($inactiveUser->email) ?></td> <td><?= $inactiveUser->active ? 'Yes' : 'No' ?></td> </tr> <?php endforeach; ?> </tbody> </table> <?= $this->Paginator->counter() ?> <?= $this->Paginator->prev('« Previous') ?> <?= $this->Paginator->numbers() ?> <?= $this->Paginator->next('Next »') ?> And with this you have two paginated tables in the same request.

Clean DI in CakePHP 5.3: Say Goodbye to fetchTable()

This article is part of the CakeDC Advent Calendar 2025 (December 23rd, 2025)

Introduction: The Death of the "Hidden" Dependency

For years, accessing data in CakePHP meant "grabbing" it from the global state. Whether using TableRegistry::getTableLocator()->get() or the LocatorAwareTrait’s $this->fetchTable(), your classes reached out to a locator to find what they needed. While convenient, this created hidden dependencies. A class constructor might look empty, despite the class being secretly reliant on multiple database tables. This made unit testing cumbersome, forcing you to stub the global TableLocator just to inject a mock. CakePHP 5.3 changes the game with Inversion of Control. With the framework currently in its Release Candidate (RC) stage and a stable release expected soon, now is the perfect time to explore these architectural improvements. By using the new TableContainer as a delegate for your PSR-11 container, tables can now be automatically injected directly into your constructors. This shift to explicit dependencies makes your code cleaner, fully type-hinted, and ready for modern testing standards. The Old Way (Hidden Dependency): public function execute() { $users = $this->fetchTable('Users'); // Where did this come from? } The 5.3 Way (Explicit Dependency): public function __construct(protected UsersTable $users) {} public function execute() { $this->users->find(); // Explicit and testable. }

Enabling the Delegate

Open src/Application.php and update the services() method by delegating table resolution to the TableContainer. // src/Application.php use Cake\ORM\TableContainer; public function services(ContainerInterface $container): void { // Register the TableContainer as a delegate $container->delegate(new TableContainer()); }

How it works under the hood

When you type-hint a class ending in Table (e.g., UsersTable), the main PSR-11 container doesn't initially know how to instantiate it. Because you've registered a delegate, it passes the request to the TableContainer, which then:
  1. Validates: It verifies the class name and ensures it is a subclass of \Cake\ORM\Table.
  2. Locates: It uses the TableLocator to fetch the correct instance (handling all the usual CakePHP ORM configuration behind the scenes).
  3. Resolves: It returns the fully configured Table object back to the main container to be injected.
Note: The naming convention is strict. The TableContainer specifically looks for the Table suffix. If you have a custom class that extends the base Table class but is named UsersRepository, the delegate will skip it, and the container will fail to resolve the dependency.

Practical Example: Cleaner Services

Now, your domain services no longer need to know about the LocatorAwareTrait. They simply ask for what they need. namespace App\Service; use App\Model\Table\UsersTable; class UserManagerService { // No more TableRegistry::get() or $this->fetchTable() public function __construct( protected UsersTable $users ) {} public function activateUser(int $id): void { $user = $this->users->get($id); // ... logic } } Next, open src/Application.php and update the services() method by delegating table resolution to the TableContainer. // src/Application.php use App\Model\Table\UsersTable; use App\Service\UserManagerService; use Cake\ORM\TableContainer; public function services(ContainerInterface $container): void { // Register the TableContainer as a delegate $container->delegate(new TableContainer()); // Register your service with the table as constructor argument $container ->add(UserManagerService::class) ->addArgument(UsersTable::class); }

Why this is a game changer for Testing

Because the table is injected via the constructor, you can now swap it for a mock effortlessly in your test suite without touching the global state of the application. $mockUsers = $this->createMock(UsersTable::class); $service = new UserManagerService($mockUsers); // Pure injection!

Conclusion: Small Change, Big Impact

At first glance, adding a single line to your Application::services() method might seem like a minor update. However, TableContainer represents a significant shift in how we approach CakePHP architecture. By delegating table resolution to the container, we gain:
  • True Type-Safety: Your IDE and static analysis tools now recognize the exact Table class being used. This is a massive win for PHPStan users—no more "Call to an undefined method" errors or messy @var docblock workarounds just to prove to your CI that a method exists.
  • Zero-Effort Mocking: Testing a service no longer requires manipulating the global TableRegistry state. Simply pass a mock object into the constructor and move on.
  • Standardization: Your CakePHP code now aligns with modern PHP practices found in any PSR-compliant ecosystem, making your application more maintainable and easier for new developers to understand.
If you plan to upgrade to CakePHP 5.3 upon its release, this is one of the easiest wins for your codebase. It’s time to stop fetching your tables and start receiving them. This article is part of the CakeDC Advent Calendar 2025 (December 23rd, 2025)

The new CakePHP RateLimitMiddleware

This article is part of the CakeDC Advent Calendar 2025 (December 21st 2025) Rate limiting a specific endpoint of your application can be a life saver. Sometimes you can't optimize the endpoint and it'll be expensive in time or CPU, or the endpoint has a business restriction for a given user. In the past, I've been using https://github.com/UseMuffin/Throttle a number of times to provide rate limiting features to CakePHP. Recently, I've been watching the addition of the RateLimitMiddleware to CakePHP 5.3, I think it was a great idea to incorporate these features into the core and I'll bring you a quick example about how to use it in your projects. Let's imagine you have a CakePHP application with an export feature that will take some extra CPU to produce an output, you want to ensure the endpoint is not abused by your users. In order to limit the access to the endpoint, add the following configuration to your config/app.php // define a cache configuration, Redis could be a good option for a fast and distributed approach 'rate_limit' => [ 'className' => \Cake\Cache\Engine\RedisEngine::class, 'path' => CACHE, 'url' => env('CACHE_RATE_LIMIT_URL', null), ], Then, in your src/Application.php middleware method, create one or many configurations for your rate limits. The middleware allows a lot of customization, for example to select the strategy, or how are you going to identify the owner of the rate limit. ->add(new RateLimitMiddleware([ 'strategy' => RateLimitMiddleware::STRATEGY_FIXED_WINDOW, 'identifier' => RateLimitMiddleware::IDENTIFIER_IP, 'limit' => 5, 'window' => 10, 'cache' => 'rate_limit', 'skipCheck' => function ($request) { return !( $request->getParam('controller') === 'Reports' && $request->getParam('action') === 'index' ); } ])) In this particular configuration we are going to limit the access to the /reports/index endpoint (we skip everything else) to 5 requests every 10 seconds. You can learn more about the middleware configuration here https://github.com/cakephp/docs/pull/8063 while the final documentation is being finished. This article is part of the CakeDC Advent Calendar 2025 (December 21st 2025)

We Bake with CakePHP