Giter VIP home page Giter VIP logo

cakephp-service-layer's Introduction

A Service Layer for CakePHP

Software License Build Status Coverage Status Code Quality Latest Stable Version Minimum PHP Version

This is more a design pattern and conceptual idea than a lot of code and will improve the maintainability of your code base. This plugin just provides some classes to help you applying this concept in the CakePHP framework following the way of the framework of convention over configuration.

Supported CakePHP Versions

This branch is for use with CakePHP 5.0+. For details see version map.

Introduction

The rule of thumb in any MVC framework is basically "fat models, skinny controllers".

While this works pretty well the abstraction can be done even better by separating for example the DB operations from the actual business logic. Most Cake developers probably use the table objects as a bucket for everything. This is, strictly speaking, not correct. Business logic doesn't belong into the context of a DB table and should be separated from any persistence layer. CakePHP likes to mix persistence with business logic. Very well written business logic would be agnostic to any framework. You just use the framework to persists the results of your business logic.

A table object should just encapsulate whatever is in the direct concern of that table. Queries related to that table, custom finders and so on. Some of the principles we want to follow are separation of concerns and single responsibility. The Model folder in CakePHP represents the Data Model and should not be used to add things outside of this conern to it. A service layer helps with that.

The service class, a custom made class, not part of the CakePHP framework, would implement the real business logic and do any kind of calculations or whatever else logic operations need to be done and pass the result back to the controller which would then pass that result to the view.

This ensures that each part of the code is easy to test and exchange. For example the service is as well usable in a shell app because it doesn't depend on the controller. If well separated you could, in theory, have a plugin with all your table objects and share it between two apps because the application logic, specific to each app, would be implemented in the service layer not in the table objects.

Martin Fowler's book "Patterns of Enterprise Architecture" states:

The easier question to answer is probably when not to use it. You probably don't need a Service Layer if your application's business logic will only have one kind of client - say, a user interface - and it's use case responses don't involve multiple transactional resources. [...]

But as soon as you envision a second kind of client, or a second transactional resource in use case responses, it pays to design in a Service Layer from the beginning.

It's opinionated

There is a simple paragraph on this page that explains pretty well why DDD in MVC is a pretty abstract and very opinionated topic:

According to Eric Evans, Domain-driven design (DDD) is not a technology or a methodology. It’s a different way of thinking about how to organize your applications and structure your code. This way of thinking complements very well the popular MVC architecture. The domain model provides a structural view of the system. Most of the time, applications don’t change, what changes is the domain. MVC, however, doesn’t really tell you how your model should be structured. That’s why some frameworks don’t force you to use a specific model structure, instead, they let your model evolve as your knowledge and expertise grows.

CakePHP doesn't feature a template structure of any DDD or service layer architecture for that reason. It's basically up to you.

This plugin provides you one possible implementation. It's not carved in stone, nor do you have to agree with it. Consider this plugin as a suggestion or template for the implementation and as a guidance for developers who care about maintainable code but don't know how to further improve their code base yet.

How to use it

CakePHP by default uses locators instead of a dependency injection container. This plugin gives you a CakePHP fashioned service locator and a trait so you can simply load services anywhere in your application by using the trait.

The following example uses a SomeServiceNameService class:

use Burzum\CakeServiceLayer\Service\ServiceAwareTrait;

class AppController extends Controller
{
    use ServiceAwareTrait;
}

class FooController extends AppController
{
    public function initialize()
    {
        parent::initialize();
        $this->loadService('Articles');
    }

    /**
     * Get a list of articles for the current logged in user
     */
    public function index()
    {
        $this->set('results', $this->Articles->getListingForUser(
            $this->Auth->user('id')
            $this->getRequest()->getQueryParams()
        ));
    }
}

If there is already a property with the name of the service used in the controller a warning will be thrown. In an ideal case your controller won't have to use any table instances anyway when using services. The tables are not a concern of the controller.

The advantage of the above code is that the args passed to the service could come from shell input or any other source. The logic isn't tied to the controller nor the model. Using proper abstraction, the underlying data source, a repository that is used by the service, should be transparently replaceable with any interface that matches the required implementation.

You can also load namespaced services:

// Loads BarService from MyPlugin and src/Service/Foo/
$this->loadService('MyPlugin.Foo/Bar');

Make sure to get IDE support using the documented IdeHelper enhancements.

For details see docs.

Why no DI container?

You could achieve the very same by using a DI container of your choice but there was never really a need to do so before, the locators work just fine as well and they're less bloat than adding a full DI container lib. There was no need to add a DI container to any CakePHP app in the past ~10 years for me, not even in big projects with 500+ tables. One of the core concepts of CakePHP is to go by conventions over wiring things together in a huge DI config or using a container all over the place that is in most cases anyway just used like a super global bucket by many developers.

This is of course a very opinionated topic, so if you disagree and want to go for a DI container, feel free to do so! It's awesome to have a choice!

DI plugins for CakePHP:

You might find more DI plugins in the Awesome CakePHP list of plugins.

Demo

The sandbox showcases a live demo. Check the publically available code for details.

License

Copyright Florian Krämer

Licensed under The MIT License Redistributions of files must retain the above copyright notice.

cakephp-service-layer's People

Contributors

burzum avatar davidyell avatar dereuromark avatar nickbusey avatar predragleka avatar tabtyrell avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

cakephp-service-layer's Issues

CakePHP 4.4.15 - I seem to have a problem with the Pagination class on the Pagination service

Hello, I seem to have a problem

with the Pagination class on the Pagination service

  • I get this error message Screenshot_20230728_122506

However, I seem to have respected the documentation.

Here are the different files and configurations I use

  • I do not provide the view file because in view of the error message, the latter is detected in a Burzum file
// config/app.php

use Burzum\CakeServiceLayer\Annotator\ClassAnnotatorTask\ServiceAwareClassAnnotatorTask;
use Burzum\CakeServiceLayer\Generator\Task\ServiceTask;
use Cake\Cache\Engine\FileEngine;
use Cake\Database\Connection;
use Cake\Database\Driver\Mysql;
use Cake\Log\Engine\FileLog;
use Cake\Mailer\Transport\MailTransport;

  'IdeHelper' => [
        'generatorTasks' => [
            ServiceTask::class
        ],
        'classAnnotatorTasks' => [
            ServiceAwareClassAnnotatorTask::class
        ],
    ],
// src/Application.php

use Authentication\AuthenticationService;
use Authentication\AuthenticationServiceInterface;
use Authentication\AuthenticationServiceProviderInterface;
use Authentication\Identifier\IdentifierInterface;
use Authentication\Middleware\AuthenticationMiddleware;
use Cake\Core\Configure;
use Cake\Core\ContainerInterface;
use Cake\Datasource\FactoryLocator;
use Cake\Error\Middleware\ErrorHandlerMiddleware;
use Cake\Http\BaseApplication;
use Cake\Http\Middleware\BodyParserMiddleware;
use Cake\Http\Middleware\CsrfProtectionMiddleware;
use Cake\Http\MiddlewareQueue;
use Cake\ORM\Locator\TableLocator;
use Cake\Routing\Middleware\AssetMiddleware;
use Cake\Routing\Middleware\RoutingMiddleware;
use Cake\Routing\Router;
use Psr\Http\Message\ServerRequestInterface;


    public function bootstrap(): void
    {
        // Call parent to load bootstrap from files.
        parent::bootstrap();

        if (PHP_SAPI === 'cli') {
            $this->bootstrapCli();
            try {
                $this->addPlugin('IdeHelper');
            } catch (\Throwable $e) {
            }
        } else {
            FactoryLocator::add(
                'Table',
                (new TableLocator())->allowFallbackClass(false)
            );
        }

        /*
         * Only try to load DebugKit in development mode
         * Debug Kit should not be installed on a production system
         */
        if (Configure::read('debug')) {
            $this->addPlugin('DebugKit');
        }

        // Load more plugins here
        $this->addPlugin('Authentication');
        $this->addPlugin('BootstrapUI');
        $this->addPlugin('Cake/Queue');
        $this->addPlugin('Muffin/Trash'); // soft-deletion
        $this->addPlugin('Burzum/CakeServiceLayer');
    }
// src/Service/MyUsersService.php

<?php

declare(strict_types=1);

namespace App\Service;

// use App\Model\Table\UsersTable;
use Burzum\CakeServiceLayer\Service\ServiceAwareTrait;
use Cake\Http\ServerRequest;
use Cake\ORM\Locator\LocatorAwareTrait;

class MyUsersService
{
    use LocatorAwareTrait, ServiceAwareTrait;

    protected $request;
    protected $Users;

    public function __construct(ServerRequest &$request)
    {
        $this->request = $request;
        $this->loadService('Pagination', [$request]);
        // $this->loadModel('Users'); // Undefined method loadModel I'm using CakePHP 4.4.15
        $this->Users = $this->fetchTable('Users');
    }

    public function getListForUser($userId, $queryParams)
    {
        $query = $this->Users->find();
        return $this->Pagination->paginate($query, $queryParams); // here I get the Intelliphense message : Undefined Property '$Pagination'.
    }
}
// src/Controller/UsersController.php

    use MailerAwareTrait, ServiceAwareTrait;

    public const ACT_NOTIFY = 'notify';
    public const ACT_IMPERSONATE = 'impersonate';

    public function initialize(): void
    {
        parent::initialize();

        $this->loadService('MyUsers', [$this->getRequest()]);
        $this->paginate += [
            'contain' => ['Roles'],
            'limit' => 3, // 20 by default
        ];
    }

    public function index()
    {
        $users =
            $this->MyUsers->getListForUser(
                $this->getLoggedInUserAsUser()->id,
                $this->getRequest()
            );
        $this->set('users', 'paginationKeys');
    }

Here are the questions

  • After re-reading and double-checking the code against the documentation several times.
    • Especially the use of "USE" and "TRAIT".
  • I have to admit to being stuck.
  • I even copied the PaginationService.php file into my own Service directory
    • Screenshot_20230728_131301
    • that action generate an error message when running : composer annotate :
      • PHP Fatal error: Cannot declare class Burzum\CakeServiceLayer\Service\PaginationService, because the name is already in use in /var/www/html/src/Service/PaginationService.
    • So I removed it from my repertory.
    • For information here is the reult of same command
    • Screenshot_20230728_132041
  • Did I miss a detail from the documentation?
  • Did I misinterpret or misunderstand something?
  • Any explanation and/or help will be greatly appreciated.

Lets talk about DI

I am OK with the constructor args for simple use cases.
But if you want to reuse a certain object you cannot just always manually pass it in.

How could we make the following work (actual controller code) in your service concept:

$connection = new Guzzle();
$git = new Git($connection);
$releaser = new Releaser($git, $this->getMailer('Notification'));
...

Ideally I would only do

$releaser = $this->loadService('Releaser');
...

The rest is wired up outside of this controller scope

Also, I should be able to mock e.g. Guzzle or Git class methods in tests.

Currently it looks more like this everywhere:

$git = $this->getMockBuilder(Git::class)->dis...->getMock();
$git->expects($this->once())->method('moduleExists')->willReturn(true);

$mailer = $this->getMockBuilder(Mailer::class)->getMock();
$releaser = new Releaser($git, $mailer);
...

Is it compatible with cakephp 4.4.7 ?

I am trying to implement the plugin on cake version 4.4.7 and I found several incompatibilities.
Does this version is compatible with recent cakephp ?

Thank you

Mocking

Refs #4

I would like to discuss the best stategy of mocking more deeply nested API calls and stuff
(and no, phpvcr and other tools are not feasable in those cases).
With current cake semi-DI and and integration testing this is just not possible.

I modified this library a bit to allow for the following two alternatives of mocking.
One is using a god like container via Configure to transport mocks, the other one is probably a bit cleaner, using a special test-only MockedServiceLocator instead of default ServiceLocator.

Similar things could also be done for TableRegistry and other classes I guess?
Whats the feedback on those? Or what is a better alternative?

How the test works:

  • the API just returns a string, and I mock the API result in a 2nd part to return sth else.
namespace App\Test\TestCase\Controller;

use App\Controller\DemoController;
use App\Service\Bar\BazService;
use Burzum\Cake\TestSuite\MockedServiceLocator;
use Cake\Core\Configure;
use Cake\TestSuite\IntegrationTestCase;

    /**
     * Test index method
     *
     * @return void
     */
    public function testIndex()
    {
        $this->disableErrorHandlerMiddleware();

        $this->get('/demo');

        $this->assertResponseCode(200);

        $x = Configure::read('test-string');
        $this->assertSame('original', $x);
    }

    /**
     * Test index method
     *
     * @return void
     */
    public function testIndexMocked()
    {
        $this->disableErrorHandlerMiddleware();

        $mockedService = $this->getMockBuilder(BazService::class)->setMethods(['doSth'])->getMock();
        $mockedService->expects($this->once())->method('doSth')->willReturn('faked');

        Configure::write('Services.' . 'Bar/Baz', $mockedService);

        $this->get('/demo');

        $this->assertResponseCode(200);

        $x = Configure::read('test-string');
        $this->assertSame('faked', $x);
    }

    /**
     * Test index method
     *
     * @return void
     */
    public function testIndexMockedAlternative()
    {
        DemoController::setDefaultServiceLocator(MockedServiceLocator::class);

        $this->disableErrorHandlerMiddleware();

        $mockedService = $this->getMockBuilder(BazService::class)->setMethods(['doSth'])->getMock();
        $mockedService->expects($this->once())->method('doSth')->willReturn('faked');

        MockedServiceLocator::mock(BazService::class, $mockedService);

        $this->get('/demo');

        $this->assertResponseCode(200);

        $x = Configure::read('test-string');
        $this->assertSame('faked', $x);
    }

Call to a plugin's Service results in an error

I'm migrating from CakePHP4.5 to 5.0.
There is a plugin which contains a Service.
In a Table of a project, i initialize the service as:

class SomeTable extends Table
{
    use ServiceAwareTrait;

    public function initialize(array $config): void
    {
        parent::initialize($config);
        $this->loadService('SomePlugin.Foo');
    }
}

Which results in this error:
__construct(): Argument #1 ($config) must be of type array, string given

When i dump $args in ServiceLocator on line 83, it shows SomePlugin.Foo.
Somehow the name of the Service is handled as $config.

Workaround:

        $this->loadService('SomePlugin.Foo', ['config' => []]);

This might be related to: cakephp/cakephp#17435

Problem defining class name

$this->loadService('ProductsService', ['className' => 'Products']);

Get error: 500
Cannot unpack array with string keys

Problem using Pagination, possible bootstrap.php fix

Hi,

I'm having problem to paginate a table inside a Service.

I found the https://github.com/burzum/cakephp-service-layer/blob/master/config/bootstrap.php need to update, to respect immutable behavior of request objected. But in my local machine, I only made this work when also replace $controller->getEventManager() to EventManager::instance() - which I think, is not the correct way.

First question is: I need to put ServicePaginatorTrait on controller? If so, I cannot paginate inside my service without bound it with ServerRequest object?

Last question: should I open a PR with proposed bootstrap fix?

Service

class HandsetsService
{
    use ModelAwareTrait;
    use ServicePaginatorTrait;

    /**
     * @var \Cake\ORM\Table
     */
    protected $Handsets;

    /**
     * Constructor
     */
    public function __construct()
    {
        $this->initialize();
    }

    /**
     * Initialize
     *
     * @return void
     */
    public function initialize()
    {
        $this->Handsets = $this->loadModel('Handsets');
    }

    /**
     * Listagem paginada dos aparelhos
     *
     * @return \Cake\Datasource\ResultSetInterface|array
     */
    public function listing()
    {
        $query = $this->Handsets->find('enableds');
        $query->contain(['Users']);

        $result = $this->paginate($query);

        return $result;
    }
}

Controller

class HandsetsController extends AppController
{
    use ServiceAwareTrait;

    public function initialize(): void
    {
        $this->loadService('Handsets');
    }

    /**
     * Index method
     *
     * @return \Cake\Http\Response|null|void Renders view
     */
    public function index()
    {
        $handsets = $this->Handsets->listing();

        $this->set(compact('handsets'));
    }
}

Local working bootstrap

$eventManager = EventManager::instance();
$eventManager->on('Controller.initialize', function ($event) use ($eventManager) {
    $controller = $event->getSubject();
    $eventManager->on('Service.afterPaginate', function ($event) use ($controller) {
        $controller->setRequest($event->getSubject()->addPagingParamToRequest($controller->getRequest()));
    });
});

Maybe, correct bootstrap

EventManager::instance()->on('Controller.initialize', function ($event) {
    $controller = $event->getSubject();
    $controller->getEventManager()->on('Service.afterPaginate', function ($event) use ($controller) {
        $controller->setRequest($event->getSubject()->addPagingParamToRequest($controller->getRequest()));
    });
});

loadService() issue

We had loadService() in a loop, expecting it to behave like loadModel() just returning the service if already created.

Warning (512): App\Release\Releaser::$%s is already in use. in [/.../vendor/burzum/cakephp-service-layer/src/Service/ServiceAwareTrait.php, line 66]

Do you think we can just

return $this->{$name} 

here, as well?

if (isset($this->{$name})) {
    //trigger_error(__CLASS__ . '::$%s is already in use.', E_USER_WARNING);
    return $this->{$name};
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.