jot/hf-repository 问题修复 & 功能扩展

解决BUG、新增功能、兼容多环境部署,快速响应你的开发需求

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

jot/hf-repository

Composer 安装命令:

composer require jot/hf-repository

包简介

Projects related entities and repositories for elasticsearch mappings

README 文档

README

O hf-repository é uma library extensível para gerenciamento de dados, oferecendo uma camada de abstração baseada no conceito de Repositories. Ele foi projetado para trabalhar de forma eficiente com o Elasticsearch, utilizando o padrão de design Repository Pattern para isolar a lógica de acesso aos dados. Simplifica a interação com índices, facilita o mapeamento de resultados para entidades e fornece suporte confiável para operações CRUD, busca avançada, paginação e geração de consultas dinâmicas.

Recursos Principais

  • Abstração de Repositórios: Camada central para comunicação com o Elasticsearch.
  • CRUD Completo: Operações básicas para criar, buscar, atualizar e remover itens.
  • Paginação Simplificada: Paginador nativo com alta personalização de parâmetros.
  • Mapeamento de Entidades: Integra resultados aos objetos do domínio do projeto.
  • Flexibilidade de Consultas: Filtros, ordenações e seleção de atributos via consulta.
  • Fácil Extensão: Estrutura modular para criação de repositórios específicos.
  • Gerador de código: Comandos inteligentes para criar as entidades, repositórios e controladores, construindo um CRUD completo para as chamadas de API.

Estrutura do Projeto

O projeto segue uma arquitetura limpa e bem organizada:

  • Entidades (Entity): Representam os dados do negócio e oferecem funcionalidades para manipulação e hidratação de valores.
  • Repositórios (Repository): Realizam todas as interações com a camada de dados, encapsulando as operações.
  • Construtor de Consultas (QueryBuilder): Fornece suporte à criação e execução de consultas dinâmicas no Elasticsearch.

Tecnologias Utilizadas

  • Hyperf Framework: Utilizado como base para o projeto, com suporte ao paradigma reativo e arquitetura moderna.
  • Elasticsearch: Banco de dados NoSQL utilizado para indexação e busca avançada.
  • Redis: Armazenamento em memória de alta performance utilizado para caching, controle do rate-limit e gerenciamento de sessões, otimizando a eficiência e performance do sistema.
  • PSR-11 (Container): Integração do padrão de contêiner de dependências para maior compatibilidade.
  • PHP 8.1+: Versão mínima do PHP, aproveitando recursos modernos como atributos e tipagem forte.

Instalação

Para integrar o hf-repository no seu projeto Hyperf adicione as dependências necessárias ao seu projeto (via composer):

composer require jot/hf-repository

Após a instalação, publique os arquivos de configuração das dependências necessárias:

php bin/hyperf.php vendor:publish hyperf/redis
php bin/hyperf.php vendor:publish hyperf/rate-limit
php bin/hyperf.php vendor:publish hyperf/swagger
php bin/hyperf.php vendor:publish jot/hf_elastic

Exemplo de Uso

O comando mais importante desta biblioteca é a criação de um CRUD completo a partir de um índice do Elasticsearch.

O comando repo:crud vai criar as classes de entidades, repositório e controlador, além de já preparar toda a documentação do swagger e aplicar um rate limit padrão de 10 requisições por segundo.

php bin/hyperf.php repo:crud --index=orders
You are about to create a CRUD for index orders with api version v1.
The elasticsearch index related entities, repository and controller will be created during this process.

Are you sure you want to create a CRUD for index orders? [Y/n] [Y]:
 
Creating the CRUD for index orders...

Start creating entities...
+ [OK] ./app/Entity/Order/Customer.php
+ [OK] ./app/Entity/Order/Invoice.php
+ [OK] ./app/Entity/Order/Item.php
+ [OK] ./app/Entity/Order/OrderHistory.php
+ [OK] ./app/Entity/Order/Payment.php
+ [OK] ./app/Entity/Order/Shipment.php
+ [OK] ./app/Entity/Order/Order.php

Start creating repository...
+ [OK] ./app/Repository/OrderRepository.php

Start creating controller...
+ [OK] ./app/Controller/V1/OrderController.php

Os detalhes de cada classe gerada por este comando serão explicadas no próximo tópico.

Criando as classes individualmente

Para criar individualmente as classes necessárias, siga os comandos na ordem abaixo:

1. Criando dinamicamente as classes das entidades

Utilize o comando de console repo:entity para gerar as entidades relacionadas ao índice.

php bin/hyperf.php repo:entity --index=orders
+ [OK] ./app/Entity/Order/Customer.php
+ [OK] ./app/Entity/Order/Invoice.php
+ [OK] ./app/Entity/Order/Item.php
+ [OK] ./app/Entity/Order/OrderHistory.php
+ [OK] ./app/Entity/Order/Payment.php
+ [OK] ./app/Entity/Order/Shipment.php
+ [OK] ./app/Entity/Order/Order.php

Conforme demonstrado, o comando vai analisar o mapping do índice informado e vai criar a entidade principal do índice e caso haja objetos e objetos nested no índice, uma classe de entidade será gerada para cada um deles.

Cada classe gerada já vem aplicada com as configurações do Swagger, criando as referências necessárias para a documentação do projeto.

<?php

declare(strict_types=1);

namespace Jot\HfShield\Entity\Order;

use Jot\HfRepository\Entity;
use Jot\HfRepository\Trait\HasTimestamps;
use Jot\HfRepository\Trait\HasLogicRemoval;
use Hyperf\Swagger\Annotation as SA;

#[SA\Schema(schema: "app.entity.order.order")]
class Order extends Entity
{

    use HasLogicRemoval, HasTimestamps;

        #[SA\Property(
        property: "created_at",
        type: "string",
        format: "string",
        readOnly: true,
        x: ["php_type" => "\DateTime"]
    )]
    protected ?\DateTime $createdAt = null;

    #[SA\Property(
        property: "customer",
        ref: "#/components/schemas/app.entity.order.customer",
        x: ["php_type" => "\Jot\HfShield\Entity\Order\Customer"]
    )]
    protected ?\Jot\HfShield\Entity\Order\Customer $customer = null;

    #[SA\Property(
        property: "id",
        type: "string",
        example: "749ef2bd-1372-4ef2-998c-0cbec9bc1496"
    )]
    protected ?string $id = null;

    #[SA\Property(
        property: "installment_count",
        type: "integer",
        example: 5
    )]
    protected ?int $installmentCount = null;

    // ...

}

2. Criando o repositório

Para criar o repositório relacionado ao índice do elasticsearch, utilize o comando abaixo:

php bin/hyperf.php repo:repository --index=orders
+ [OK] ./app/Repository/OrderRepository.php

O comando criará dentro do diretório app/Repository uma classe User com o seguinte conteúdo:

<?php

namespace App\Repository;

use Jot\HfRepository\Repository;
use Jot\HfShield\Entity\Order\Order as Entity;

class OrderRepository extends Repository
{
    protected string $entity = Entity::class;

}

3. Criando um controlador que fará uso do repositório

O comando repo:controller vai criar um controlador OrderController já preparado para receber os métodos GET, POST, PUT, DELETE e HEAD com as consultas e persistências realizadas pelo repositório gerado anteriormente.

php bin/hyperf.php repo:controller --index=orders
+ [OK] ./app/Controller/V1/OrderController.php

Repare que o diretório final do controlador é baseada em uma versão. É possível definir a versão da api acrescentando a opção --api-version= ao comando.

php bin/hyperf.php repo:controller --index=orders --api-version=v2

Caso o arquivo do controlador exista, o comando perguntará se deseja substituí-lo.

php bin/hyperf.php repo:controller --index=orders
The file ./app/Controller/V1/OrderController.php already exists. Overwrite file? [y/n/a] [n]:

Também é possível já forçar a substituição do arquivo diretamente no comando usando a opção --force

php bin/hyperf.php repo:controller --index=orders --force

O arquivo do controlador gerado também vem com as configurações do Swagger aplicadas. Além disso, também são aplicadas configurações de rate limit para limitar as requisições à API.

<?php

declare(strict_types=1);

namespace App\Controller\V1;

use App\Controller\AbstractController;
use Jot\HfShield\Entity\Order\Order;
use App\Repository\OrderRepository;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Annotation\Controller;
use Hyperf\HttpServer\Annotation\DeleteMapping;
use Hyperf\HttpServer\Annotation\GetMapping;
use Hyperf\HttpServer\Annotation\PostMapping;
use Hyperf\HttpServer\Annotation\PutMapping;
use Hyperf\RateLimit\Annotation\RateLimit;
use Hyperf\Swagger\Annotation as SA;
use Psr\Http\Message\ResponseInterface as PsrResponseInterface;

#[SA\HyperfServer('http')]
#[SA\Tag(
    name: 'Order',
    description: 'Endpoints related to orders management'
)]
#[SA\Schema(schema: "app.error.response", required: ["result", "error"],
    properties: [
        new SA\Property(property: "result", type: "string", example: "error"),
        new SA\Property(property: "error", type: "string", example: "Error message"),
        new SA\Property(property: "data", type: "string|array", example: null),
    ],
    type: "object"
)]
#[Controller(prefix: '/v1')]
class OrderController extends AbstractController
{

    #[Inject]
    protected OrderRepository $repository;

    #[SA\Get(
        path: "/orders",
        description: "Retrieve a list of orders with optional pagination.",
        summary: "Get Orders List",
        tags: ["Order"],
        parameters: [
            new SA\Parameter(
                name: "_page",
                description: "Page number for pagination",
                in: "query",
                required: false,
                schema: new SA\Schema(type: "integer", example: 1)
            ),
            new SA\Parameter(
                name: "_per_page",
                description: "Number of results per page",
                in: "query",
                required: false,
                schema: new SA\Schema(type: "integer", example: 10)
            ),
            new SA\Parameter(
                name: "_sort",
                description: "Sort results by a specific fields",
                in: "query",
                required: false,
                schema: new SA\Schema(type: "string", example: "created_at:desc,updated_at:desc")
            ),
            new SA\Parameter(
                name: "_fields",
                description: "Fields to include in the response",
                in: "query",
                required: false,
                schema: new SA\Schema(type: "string", example: "id,created_at,updated_at")
            )
        ],
        responses: [
            new SA\Response(
                response: 200,
                description: "Order details retrieved successfully",
                content: new SA\JsonContent(
                    properties: [
                        new SA\Property(
                            property: "data",
                            type: "array",
                            items: new SA\Items(ref: "#/components/schemas/app.entity.order.order")
                        ),
                        new SA\Property(
                            property: "result",
                            type: "string",
                            example: "success"
                        ),
                        new SA\Property(
                            property: "error",
                            type: "string",
                            example: null,
                            nullable: true
                        )
                    ],
                    type: "object"
                )
            ),
            new SA\Response(
                response: 400,
                description: "Bad Request",
                content: new SA\JsonContent(ref: "#/components/schemas/app.error.response")
            ),
            new SA\Response(
                response: 500,
                description: "Application Error",
                content: new SA\JsonContent(ref: "#/components/schemas/app.error.response")
            )
        ]
    )]
    #[RateLimit(create: 10, consume: 1)]
    #[GetMapping('orders[/]')]
    public function getOrdersList(): PsrResponseInterface
    {
        $response = $this->repository->paginate($this->request->all());
        if ($response['result'] === 'error') {
            return $this->response->withStatus(400)->json($response);
        }
        return $this->response
            ->withHeader('Access-Control-Allow-Origin', '*')
            ->json($response);

    }

    #[SA\Get(
        path: "/orders/{id}",
        description: "Retrieve the details of a specific orders identified by ID.",
        summary: "Get Order Data",
        tags: ["Order"],
        parameters: [
            new SA\Parameter(
                name: "id",
                description: "Unique identifier of the orders",
                in: "path",
                required: true,
                schema: new SA\Schema(type: "string", example: "12345")
            )
        ],
        responses: [
            new SA\Response(
                response: 200,
                description: "Order details retrieved successfully",
                content: new SA\JsonContent(
                    properties: [
                        new SA\Property(
                            property: "data",
                            ref: "#/components/schemas/app.entity.order.order"
                        ),
                        new SA\Property(
                            property: "result",
                            type: "string",
                            example: "success"
                        ),
                        new SA\Property(
                            property: "error",
                            type: "string",
                            example: "Invalid request parameters",
                            nullable: true
                        )
                    ],
                    type: "object"
                )
            ),
            new SA\Response(
                response: 400,
                description: "Server Error",
                content: new SA\JsonContent(ref: "#/components/schemas/app.error.response")
            ),
            new SA\Response(
                response: 404,
                description: "Order Not Found",
                content: new SA\JsonContent(ref: "#/components/schemas/app.error.response")
            ),
            new SA\Response(
                response: 500,
                description: "Application Error",
                content: new SA\JsonContent(ref: "#/components/schemas/app.error.response")
            )
        ]
    )]
    #[RateLimit(create: 10, consume: 1)]
    #[GetMapping('orders/{id}')]
    public function getOrderData(string $id): PsrResponseInterface
    {
        $response = $this->repository->find($id);

        if (empty($response)) {
            return $this->response->withStatus(404)->json([
                'data' => null,
                'result' => 'not-found',
                'message' => 'Document not found'
            ]);
        }

        return $this->response->json([
            'data' => $response->toArray(),
            'result' => 'success',
            'message' => null,
        ]);
    }

    #[SA\Post(
        path: "/orders",
        description: "Add a new orders to the system.",
        summary: "Create a New Order",
        requestBody: new SA\RequestBody(
            required: true,
            content: new SA\JsonContent(ref: "#/components/schemas/app.entity.order.order")
        ),
        tags: ["Order"],
        responses: [
            new SA\Response(
                response: 201,
                description: "Order Created",
                content: new SA\JsonContent(ref: "#/components/schemas/app.entity.order.order")
            ),
            new SA\Response(
                response: 400,
                description: "Bad Request",
                content: new SA\JsonContent(ref: "#/components/schemas/app.error.response")
            ),
            new SA\Response(
                response: 500,
                description: "Application Error",
                content: new SA\JsonContent(ref: "#/components/schemas/app.error.response")
            )
        ]
    )]
    #[RateLimit(create: 10, consume: 2)]
    #[PostMapping('orders[/]')]
    public function createOrder(): PsrResponseInterface
    {
        $entity = new Order($this->request->all());

        try {
            $response = $this->repository->create($entity);
        } catch (\Throwable $e) {
            return $this->response->withStatus(400)->json([
                'data' => null,
                'result' => 'error',
                'message' => $e->getMessage()
            ]);
        }

        return $this->response->withStatus(201)->json([
            'data' => $response->toArray(),
            'result' => 'success',
            'message' => null,
        ]);
    }

    #[SA\Put(
        path: "/orders/{id}",
        description: "Update the details of an existing orders.",
        summary: "Update an Order",
        requestBody: new SA\RequestBody(
            required: true,
            content: new SA\JsonContent(ref: "#/components/schemas/app.entity.order.order")
        ),
        tags: ["Order"],
        parameters: [
            new SA\Parameter(
                name: "id",
                description: "Unique identifier of the orders",
                in: "path",
                required: true,
                schema: new SA\Schema(type: "string", example: "12345")
            )
        ],
        responses: [
            new SA\Response(
                response: 200,
                description: "Order Updated",
                content: new SA\JsonContent(ref: "#/components/schemas/app.entity.order.order")
            ),
            new SA\Response(
                response: 400,
                description: "Bad Request",
                content: new SA\JsonContent(ref: "#/components/schemas/app.error.response")
            ),
            new SA\Response(
                response: 404,
                description: "Order Not Found",
                content: new SA\JsonContent(ref: "#/components/schemas/app.error.response")
            ),
            new SA\Response(
                response: 500,
                description: "Application Error",
                content: new SA\JsonContent(ref: "#/components/schemas/app.error.response")
            )
        ]
    )]
    #[RateLimit(create: 10, consume: 5)]
    #[PutMapping('orders/{id}')]
    public function updateOrder(string $id): PsrResponseInterface
    {
        $entity = new Order(['id' => $id, ...$this->request->all()]);

        try {
            $response = $this->repository->update($entity);
        } catch (\Throwable $e) {
            return $this->response->withStatus(400)->json([
                'data' => null,
                'result' => 'error',
                'message' => $e->getMessage()
            ]);
        }

        return $this->response->json([
            'data' => $response->toArray(),
            'result' => 'success',
            'message' => null,
        ]);
    }

    #[SA\Delete(
        path: "/orders/{id}",
        description: "Delete an existing orders by its unique identifier.",
        summary: "Delete an Order",
        tags: ["Order"],
        parameters: [
            new SA\Parameter(
                name: "id",
                description: "Unique identifier of the orders",
                in: "path",
                required: true,
                schema: new SA\Schema(type: "string", example: "12345")
            )
        ],
        responses: [
            new SA\Response(
                response: 200,
                description: "Order Deleted",
                content: new SA\JsonContent(
                    properties: [
                        new SA\Property(
                            property: "data",
                            type: "string",
                            nullable: true
                        ),
                        new SA\Property(
                            property: "result",
                            type: "string",
                            example: "success"
                        ),
                        new SA\Property(
                            property: "error",
                            type: "string",
                            example: "Order not found",
                            nullable: true
                        )
                    ],
                    type: "object"
                )
            ),
            new SA\Response(
                response: 400,
                description: "Bad Request",
                content: new SA\JsonContent(ref: "#/components/schemas/app.error.response")
            ),
            new SA\Response(
                response: 404,
                description: "Order Not Found",
                content: new SA\JsonContent(ref: "#/components/schemas/app.error.response")
            ),
            new SA\Response(
                response: 500,
                description: "Application Error",
                content: new SA\JsonContent(ref: "#/components/schemas/app.error.response")
            )
        ]
    )]
    #[RateLimit(create: 1, consume: 1)]
    #[DeleteMapping('orders/{id}')]
    public function deleteOrder(string $id): PsrResponseInterface
    {
        return $this->response->json([
            'data' => null,
            'result' => $this->repository->delete($id) ? 'success' : 'error',
            'message' => null,
        ]);
    }
}

jot/hf-repository 适用场景与选型建议

jot/hf-repository 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 726 次下载、GitHub Stars 达 0, 最近一次更新时间为 2025 年 01 月 14 日, 在 PHP 生态内属于活跃度较高的组件。

它主要适用于以下技术方向: 「php」 「repository」 「elasticsearch」 「entity」 「etcd」 「hyperf」 等业务场景。在实际项目中,围绕这些方向常见需要落地的问题包括:接口对接、性能调优、并发安全、与既有框架(Laravel / ThinkPHP / Yii / Webman 等)的兼容适配,以及生产环境的日志埋点与稳定性保障。

我们在过去多个企业项目中使用过 jot/hf-repository 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 jot/hf-repository 我们能提供哪些服务?
定制开发 / 二次开发

基于 jot/hf-repository 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 726
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 24
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 1
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-01-14