headio/phalcon-service-layer

Phalcon项目的服务层实现

v3.7.0 2023-02-11 09:50 UTC

README

为Phalcon 5项目提供简单的仓库服务实现

Build Status Coverage Status

介绍

此库提供了一种分层架构,可以更容易地进行单元和集成测试。

服务层处理业务逻辑,在应用层(控制器或处理器)和领域之间进行调解,与单个仓库或多个仓库进行交互。所有仓库都扩展了一个抽象的查询仓库,提供了一个类似集合的接口,具有定义良好的查询方法。因此,所有查询都被隔离在仓库层。

Phalcon ORM实现了活动记录模式,因此持久性的责任仍然由活动记录承担,这与仓库服务模式/数据映射(Doctrine)不同,在仓库服务模式/数据映射中,仓库管理实体生命周期。

如果你在字里行间阅读,你可能已经了解到这是一个提供测试性、可重用性和防止逻辑泄漏到应用层的混合解决方案。权衡是您需要编写、测试和维护一些额外的样板代码。

当然,您可以通过将数据映射器(Doctrine、Atlas ORM等)与Phalcon集成来避免这种范式。然而,对于那些喜欢Phalcon ORM性能的人来说,这个库可能很有兴趣。

依赖

  • PHP >=8.0.0 <=8.0.99
  • Phalcon 5.0.0+

安装

Composer

打开一个终端窗口并运行

composer require headio/phalcon-service-layer

用法

假设以下项目结构,让我们创建处理从存储中删除记录的层,作为一个简单的使用示例。

├── src
│   │── Module
│   │    │── Admin
│   │    │    │── Controller
│   │    │    │    │── Foo
│   │    │    │── Module.php
│   │── Domain
│   │    │── Model
│   │    │    │── Foo
│   │    │── Repository
│   │    │    │── Foo
│   │    │── Service
│   │    │    │── Foo
│   │── Provider
│   │    │── FooService
└──

注册服务提供者

在服务提供者目录/src/Provider/内创建一个新的Foo服务依赖。

declare(strict_types=1);

namespace App\Service;

use App\Domain\Repository\Foo as Repository;
use App\Domain\Service\Foo as Service;
use Headio\Phalcon\ServiceLayer\Repository\Factory;
use Phalcon\Di\ServiceProviderInterface;
use Phalcon\Di\DiInterface;

class Foo implements ServiceProviderInterface
{
    public function register(DiInterface $di): void
    {
        $di->setShared(
            'fooService',
            function () {
                $repository = Factory::create(Repository::class)
                $service = new Service($repository);

                return $service;
            }
        );
    }
}

或者,基于每个模块创建依赖。

declare(strict_types=1);

namespace App\Module\Admin;

use App\Domain\Repository\Foo as Repository;
use App\Domain\Service\Foo as Service;
use Headio\Phalcon\ServiceLayer\Repository\Factory;
use Phalcon\Di\DiInterface;
use Phalcon\Loader;
use Phalcon\Mvc\ModuleDefinitionInterface;

class Module implements ModuleDefinitionInterface
{
    /**
     * {@inheritDoc}
     */
    public function registerAutoloaders(DiInterface $container = null)
    {
    }

    /**
     * {@inheritDoc}
     */
    public function registerServices(DiInterface $container)
    {
        $container->setShared(
            'fooService',
            function () use ($container) {
                $repository = Factory::create(Repository::class);
                $service = new Service($repository);

                return $service;
            }
        );
    }
}

控制器/处理器

现在服务已经就位,控制器可以通过通过将服务注入控制器中的OnConstruct方法与应用层服务层进行交互。

namespace App\Module\Admin\Foo;

use Phalcon\Mvc\Controller;

class Foo extends Controller
{
    private FooInterface $service;

    /**
     * Inject service layer dependencies
     */
    public function onConstruct(): void
    {
        $this->service = $this->getDI()->get('fooService');
    }
}

服务层

服务层与一个仓库(或多个仓库)交互,以处理业务逻辑。在下面的示例中,服务调用删除方法(为了简单起见省略了实现)通过主键删除模型实例并返回到列表视图。

declare(strict_types=1);

namespace App\Domain\Service\Foo;

use App\Domain\Repository\FooInterface;
use App\Domain\Service\FooInterface as ServiceInterface;
use Phalcon\Di\Injectable;
use Phalcon\Http\ResponseInterface;

class Foo extends Injectable implements ServiceInterface
{
    public function __construct(private FooInterface $repository)
    {
    }

    /**
     * Delete a model instance
     */
    public function deleteModel(int $id): ResponseInterface
    {
        $model = $this->repository->findByPk($id);

        if ($this->delete($model)) {
            $this->flashSession->notice('Task completed');
            return $this->response->redirect(['for' => 'adminFoos']);
        }
    }
}

仓库

所有仓库都必须扩展抽象查询仓库并实现一个抽象方法。

declare(strict_types=1);

namespace App\Domain\Repository;

use App\Domain\Model\Foo as Model;
use App\Domain\Repository\FooInterface;
use Headio\Phalcon\ServiceLayer\Repository\QueryRepository;

class Foo extends QueryRepository implements FooInterface
{
    /**
     * Return the model name managed by this repository.
     */
    protected function getModelName(): string
    {
        return Model::class;
    }
}

“Foo”仓库可以实现额外的接口,例如“FooInterface”,为服务层提供更多的具体方法。

抽象查询仓库实现了以下仓库接口

declare(strict_types=1);

namespace Headio\Phalcon\Repository\Repository;

use Headio\Phalcon\ServiceLayer\Model\CriteriaInterface;
use Headio\Phalcon\ServiceLayer\Model\ModelInterface;
use Phalcon\Mvc\Model\ResultsetInterface;
use Phalcon\Mvc\Model\Query\BuilderInterface;

interface RepositoryInterface
{
    /**
     * Return an instance of the query criteria pre-populated
     * with the model managed by this repository.
     */
    public function createCriteria(): CriteriaInterface;

    /**
     * Return an instance of the query builder.
     */
    public function createBuilder(array $params = null): BuilderInterface;

    /**
     * Fetch a column value by query criteria from storage.
     */
    public function fetchColumn(CriteriaInterface $criteria): mixed;

    /**
     * Fetch records by query criteria from storage.
     */
    public function find(CriteriaInterface $criteria): ResultsetInterface;

    /**
     * Fetch record by primary key from storage.
     */
    public function findByPk(int $id): ModelInterface;

    /**
     * Fetch first record by query criteria from storage.
     */
    public function findFirst(CriteriaInterface $criteria): ModelInterface;

    /**
     * Fetch first record by property name from storage.
     */
    public function findFirstBy(string $property, mixed $value): ModelInterface;

    /**
     * Return the fully qualified (or unqualified) class name
     * for the model managed by this repository.
     */
    public function getModel(bool $unqualified = false): string;
}

此外,还提供了一个关系特性,用于简化处理模型关系。

declare(strict_types=1);

namespace App\Domain\Repository;

use App\Domain\Model\Foo as Model;
use App\Domain\Repository\FooInterface;
use Headio\Phalcon\ServiceLayer\Repository\QueryRepository;
use Headio\Phalcon\ServiceLayer\Repository\Traits\RelationshipTrait;

class Foo extends QueryRepository implements FooInterface
{
    use RelationshipTrait;

    /**
     * Return the model name managed by this repository.
     */
    protected function getModelName(): string
    {
        return Model::class;
    }
}

查询缓存

查询缓存是通过Phalcon的事件管理器处理的。要开始,首先在您的仓库中包含一个CacheableTrait;在缓存特性中实现了EventsAwareInterface

declare(strict_types=1);

namespace App\Domain\Repository;

use App\Domain\Model\User as Model;
use Headio\Phalcon\ServiceLayer\Model\ModelInterface;
use Headio\Phalcon\ServiceLayer\Repository\QueryRepository;
use Headio\Phalcon\ServiceLayer\Repository\Traits\CacheableTrait;
use Phalcon\Events\EventsAwareInterface;

class User extends QueryRepository implements UserInterface, EventsAwareInterface
{
    use CacheableTrait;

    /**
     * Return the model name managed by this repository.
     */
    protected function getModelName(): string
    {
        return Model::class;
    }
}

然后为您的服务层创建一个服务提供者,或者如果您想省略服务层并直接与仓库一起工作,则创建一个仓库。下面的示例使用了Phalcon的服务提供者接口。

declare(strict_types=1);

namespace App\Service;

use App\Domain\Repository\Foo as Repository;
use App\Domain\Service\Foo as Service;
use Headio\Phalcon\ServiceLayer\Cache\Listener\CacheListener;
use Headio\Phalcon\ServiceLayer\Repository\Factory;
use Phalcon\Di\ServiceProviderInterface;
use Phalcon\Di\DiInterface;

class Foo implements ServiceProviderInterface
{
    public function register(DiInterface $di): void
    {
        $di->setShared(
            'fooService',
            function () {
                $eventsManager = new EventsManager();
                // factory instantiation
                $repository = Factory::create(Repository::class);
                $repository->setEventsManager($eventsManager);
                $cacheManager = $this->get('cacheManager');
                // attach the cache event listener and inject the
                // cache manager dependency
                $eventsManager->attach(
                    'cache',
                    new CacheListener(
                        $cacheManager
                    )
                );
                $service = new Service($repository);

                return $service;
            }
        );
    }
}
缓存事件监听器

事件监听器提供了两个方法来处理缓存,请参阅以下内容。

/**
 * This event listener provides caching functionality for repositories.
 */
class CacheListener
{
    public function __construct(private ManagerInterface $manager)
    {
    }

    /**
     * Appends a cache declaration to a Phalcon query instance.
     */
    public function append(
        EventInterface $event,
        RepositoryInterface $repository,
        QueryInterface $query,
    ): QueryInterface;

    /**
     * Fetches data from cache or storage using the cache-aside
     * strategy.
     */
    public function fetch(
        EventInterface $event,
        RepositoryInterface $repository,
        array $context,
    ): ModelInterface|ResultsetInterface;
}

要触发一个缓存事件,请参阅以下来自缓存特性的具体示例。

trait CacheableTrait
{
    /**
     * Fetch first record by query criteria from cache or storage.
     *
     * @throws NotFoundException
     */
    public function findFirst(CriteriaInterface $criteria): ModelInterface
    {
        $query = $criteria
            ->createBuilder()
            ->getQuery()
            ->setUniqueRow(true)
        ;
        $this->eventsManager->fire('cache:append', $this, $query);
        $model = $query->execute();

        if (!$model instanceof ModelInterface) {
            throw new NotFoundException('404 Not Found');
        }

        return $model;
    }

    /**
     * Fetch data from cache or storage.
     */
    public function fromCache(
        QueryInterface|array $query,
        Closure $callable,
        DateInterval|int $lifetime = null,
    ): ResultsetInterface|ModelInterface|null {
        $key = $this->cacheManager->generateKey(
            $this->getModel(),
            $query,
        );

        return $this->eventsManager->fire(
            'cache:fetch',
            $this,
            [$key, $callable],
        );
    }

分页

此库提供了一个基于游标的分页适配器;请参阅测试目录中的_stub目录的使用方法。

模型

所有模型都必须扩展抽象类 Model,该类实现了以下模型接口

declare(strict_types=1);

namespace Headio\Phalcon\ServiceLayer\Model;

use Phalcon\Di\DiInterface;
use Phalcon\Mvc\Model\CriteriaInterface;

interface ModelInterface
{
    /**
     * Return the model primary key attribute.
     */
    public function getPrimaryKey(): string;

    /**
     * Return the property binding type for a given property.
     */
    public function getPropertyBindType(string $property): int;

    /**
     * Return the model validation errors as an array representation.
     */
    public function getValidationErrors(): array;

    /**
     * Return an instance of the query criteria pre-populated
     * with the model.
     */
    public static function query(DiInterface $container = null): CriteriaInterface;
}

验证

验证可以在服务层或模型类中实现。

测试

要查看测试,请运行

php vendor/bin/codecept run -f --coverage --coverage-xml

许可证

Phalcon服务层是开源的,并且根据MIT许可证授权。