Help on fat model design for CakePHP 4

Howdy.

I’ve spent a few hours searching for examples and recommendations but have yet to manage even the simplest code to move logic from my controller to my model. This is because all the examples I found relate to cake 3 (or even 2 about ModelApp and such).

So can someone throw me a bone here?!

For instance, the “business logic” of adding an article in the CMS example could be moved into a model - I know this is a simple example, but it would help to see it done.
https://book.cakephp.org/4/en/tutorials-and-examples/cms/authorization.html#fixing-the-add-edit-actions
What is there: -

// in src/Controller/ArticlesController.php

public function add()
{
    $article = $this->Articles->newEmptyEntity();
    $this->Authorization->authorize($article);

    if ($this->request->is('post')) {
        $article = $this->Articles->patchEntity($article, $this->request->getData());

        // Changed: Set the user_id from the current user.
        $article->user_id = $this->request->getAttribute('identity')->getIdentifier();

        if ($this->Articles->save($article)) {
            $this->Flash->success(__('Your article has been saved.'));
            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error(__('Unable to add your article.'));
    }
    $tags = $this->Articles->Tags->find('list');
    $this->set(compact('article', 'tags'));
}

What I would like to be able to do (and I am guessing at the object names, function names, parameters… etc: -

// in src/Controller/ArticlesController.php

public function add()
{
    $article = $this->Articles->newEmptyEntity();
    $this->Authorization->authorize($article);
    if ($this->request->is('post')) {
        if ($this->Articles->articleAdd($article, $this->request->getData())) //business logic here!
        {
            $this->Flash->success(__('Your article has been saved.'));
            return $this->redirect(['action' => 'index']);
        }
        $this->Flash->error(__('Unable to add your article.'));
    }
    $tags = $this->Articles->Tags->find('list');
    $this->set(compact('article', 'tags'));
}

//and in src/Model/Entity/ or is it a Behavior, or even in the Table? some function like
public function articleAdd($article = array(), $data = array()) 
{
    ...

thus you see how lost I am :stuck_out_tongue:

Am I misunderstanding what is to go where here?

My actual need is sending an email when that add() is('post') fires.
I.e. Common mistake #3 here https://www.toptal.com/cakephp/most-common-cakephp-mistakes (which is for an older cake!).and itself could be wrong judging by the first comment after the article!

Any help appreciated!!

Cheers
Jonathan

Here is a simple pattern you could build on:

    // in src/Model/Table/ArticlesTable.php
    public function add($user_id, $data)
    {
        $article = $this->newEntity($data);
        // patchEntity will run validation,
        // setting the property directly as you proposed would not       
        $article = $this->patchEntity(
            $article,
            ['use_id' => $user_id]
        );
        return $this->save($article);
    }
    // in src/Controller/ArticlesController.php
    public function add()
    {
        $article = $this->Articles->newEmptyEntity();
        $this->Authorization->authorize($article);

        if ($this->request->is('post')) {
            $result = $this->Articles->add(
                $this->request->getAttribute('identity')->getIdentifier(),
                $this->request->getData()
            );
            if ($result) {
                $this->Flash->success(__('Your article has been saved.'));
                return $this->redirect(['action' => 'index']);
            }
            $this->Flash->error(__('Unable to add your article.'));
        }

        $tags = $this->Articles->Tags->find('list');
        $this->set(compact('article', 'tags'));
    }

As to sending an email with the saved article, you might consider an event for that. The Model.onSave might be a simple place to include your email logic. Or if you only want the email to go out on add and not edit, you could register and write a Model.onArticleAdd event.

Further example code

You have the freedom to move more logic to the model. I don’t recommend this, but it is possible.

Controller determines post data for itself
    // in src/Model/Table/ArticlesTable.php
    public function add($user_id)
    {
        $result = null;
        $request = Router::getRequest();
        if ($request->is('post')) {
            $article = $this->newEntity($request->getData());

            $article = $this->patchEntity(
                $article,
                ['use_id' => $user_id]
            );
            $result = $this->save($article);
        }
        return $result;
    }
    // in src/Controller/ArticlesController.php
    public function add()
    {
        $article = $this->Articles->newEmptyEntity();
        $this->Authorization->authorize($article);

        $result = $this->Articles->add(
            $this->request->getAttribute('identity')->getIdentifier()
        );
        if ($result) {
            $this->Flash->success(__('Your article has been saved.'));
            return $this->redirect(['action' => 'index']);
        }
        elseif ($result === false) {
            $this->Flash->error(__('Unable to add your article.'));
        }
        // result === null, no save attempt falls through to here
        $tags = $this->Articles->Tags->find('list');
        $this->set(compact('article', 'tags'));
    }

Here is the same code rewritten to work from the posted data and the user id. This depends on the fact that in a GET, $this->request->getData() will be null.

Controller might get null data when there is no post
    /**
     * @param int $user_id
     * @param array $data
     * @return Article|false|null
     */
    public function add($user_id, $data)
    {
        if (!is_null($data)) {
            $article = $this->newEntity($data);
            $article = $this->patchEntity(
                $article,
                ['use_id' => $user_id]
            );
            $data = $this->save($article);
        }
        return $data;
    }
    // controller code is the same as above except

        $result = $this->Articles->add(
            $this->request->getAttribute('identity')->getIdentifier(),
            $this->request->getData()
        );

Thanks so much for that - none of the examples I saw put it in the /Table/ but that approach suits me!

Although you didn’t recommend passing the entity, not doing means when I am editing I need to do

$article = $this->Articles
        ->findBySlug($slug)
        ->contain('Tags') // load associated Tags
        ->firstOrFail();

twice. Once in the src/Controller/ArticlesController.php to check the $this->Authorization->authorize($article); then again in src/Model/Table/ArticlesTable.php to load the entity to patch it.
This is 2 database operations for the same record (but no doubt it’ll be cached internally, even still) so does this break the DRY principle? In which case, am I better off passing the $article as a parameter to the Table.edit()? Therefore, I would also do the same for Table.add() only because I like the consistency!

If I had an entity in one realm but needed it another, I would definitely pass it as an argument rather than recreate it.

I don’t know of any hard and fast rules for deciding how to structure code. I’m sure there are many strongly held opinions and many people who might deliver their preferred approach as though it were gospel. For me, I treat all my code as experimental and regularly refactor sections of it as my insight changes or as the requirements of the system make earlier structure clumsy.

The test I most often use to decide how to put things together is ‘will my 6-months-from-now self understand this’. That test prevents me from writing too much ‘clever’ code. :slight_smile: