bit-mx/data-entities

2.1.9 2024-09-11 16:53 UTC

README

在不使用所有样板代码的情况下,从Sqlserver中执行Laravel存储过程。

目录

简介

数据实体是一个库,允许您轻松地在Sqlserver中执行存储过程。它是Laravel的DB Facade的包装器。

安装

您可以通过composer安装此包

composer require bit-mx/data-entities

设置

您需要发布配置文件来设置连接名称。

php artisan vendor:publish --provider="BitMx\DataEntities\DataEntitiesServiceProvider" --tag="config"

此命令将在config目录中创建一个新的配置文件。

return [
    'database' => env('DATA_ENTITIES_CONNECTION', 'sqlsrv'),
];

兼容性

此包与Laravel 10.x及以上版本兼容。

由于laravel 11需要php 8.2,此包与php 8.2及以上版本兼容。

入门

创建数据实体

要创建数据实体,您需要扩展DataEntity类,并实现resolveStoreProcedure方法,其中包含您要执行的存储过程的名称。

您还可以重写defaultParameters方法来设置存储过程的默认参数。

namespace App\DataEntities;

use DataEntities\DataEntity;
use BitMx\DataEntities\Enums\Method;
use BitMx\DataEntities\Enums\ResponseType;
use BitMx\DataEntities\Responses\Response;
use Illuminate\Support\Collection;

class GetAllPostsDataEntity extends DataEntity
{
    protected ?ResponseType $responseType = ResponseType::SINGLE;
    
    public function __construct(
        protected int $authorId,
    ) 
    {
    
    }
    
    #[\Override]
    public function resolveStoreProcedure(): string
    {
        return 'spListAllPost';
    }

    #[\Override]
    public function defaultParameters(): array
    {
        return [
            'author_id' => $this->authorId,
        ];
    } 
}

您还可以使用parameters方法来设置存储过程的参数。

use App\DataEntities\GetAllPostsDataEntity;

$dataEntity = new GetAllPostsDataEntity(1);

$dataEntity->parameters()->add('tag', 'laravel');

ResponseType枚举有两个选项:SINGLE和COLLECTION。

SINGLE用于存储过程返回单行时,COLLECTION用于存储过程返回多行时。

您可以使用artisan命令创建新的数据实体

php artisan make:data-entity GetAllPostsDataEntity

此命令将在app/DataEntities目录中创建新的数据实体。

连接

您可以通过重写resolveDatabaseConnection方法来设置连接名称。

namespace App\DataEntities;

use DataEntities\DataEntity;
use BitMx\DataEntities\Enums\Method;
use BitMx\DataEntities\Enums\ResponseType;

class GetAllPostsDataEntity extends DataEntity
{
    ...
    
    #[\Override]
    public function resolveDatabaseConnection(): string
    {
        return 'sqlsrv';
    }
}

执行数据实体

要执行数据实体,您需要在数据实体实例上调用execute方法。

use App\DataEntities\GetAllPostsDataEntity;

$dataEntity = new GetAllPostsDataEntity(1);

$response = $dataEntity->execute();

$data = $response->data();

execute方法返回一个包含存储过程返回数据的Response对象。

修改器

您可以使用mutators方法在将参数发送到存储过程之前对其进行转换。

namespace App\DataEntities;

use Carbon\Carbon;use DataEntities\DataEntity;
use BitMx\DataEntities\Enums\Method;
use BitMx\DataEntities\Enums\ResponseType;

class GetAllPostsDataEntity extends DataEntity
{
    ...
    
    #[\Override]
    public function defaultParameters(): array
    {
        return [
            'date' => Carbon::now(),
        ];
    } 
    
     /**
     * @return array<string, string>
     */
     #[\Override]
    protected function mutators(): array
    {
        return [
            'date' => 'datetime:Y-m-d H:i',
        ];
    }
}

这将把日期参数转换为格式化的日期字符串,然后再发送到存储过程。

可用的修改器

  • datetime: : 使用指定的格式将值转换为日期时间字符串。您可以将格式作为参数传递给cast。示例

    • datetime 返回 Y-m-d H:i:s
    • datetime:Y-m-d
    • datetime:H:i:s
    • datetime:Y-m-d H:i:s
  • date: : 将值转换为日期 Y-m-d

  • bool: : 将值转换为整数形式的布尔值。示例:如果值为true,则将其转换为1,如果为false,则转换为0。

  • int: : 将值转换为整数。

  • float: : 将值转换为浮点数。您可以将小数位数作为参数传递给cast。示例

    - `float` Returns a float with 2 decimals.
    - `float:4` Returns a float with 4 decimals.
    - `float:0` Returns an integer.
    
  • 字符串: : 将值转换为字符串。

  • JSON: : 将值转换为JSON字符串。 : 示例

    : - 如果传入数组,则将其转换为JSON字符串。

    • [1, 2,4] 将被转换为 "[1,2,4]"
    • ['name' => 'John'] 将被转换为 '{"name":"John"}'
    • 可以将JSON选项作为参数传递给cast。
    • 'json:'. JSON_PRETTY_PRINT 将返回具有JSON_PRETTY_PRINT选项的JSON字符串。

自定义修改器

可以通过实现Mutable接口来创建自定义转换器。

namespace BitMx\DataEntities\Mutators;

use BitMx\DataEntities\Contracts\Mutable;

class CustomMutator implements Mutable
{
    /**
     * {@inheritDoc}
     */
    public function transform(string $key, mixed $value, array $parameters): mixed
    {
        
    }
}

可以使用Artisan命令创建新的cast。

php artisan make:data-entity-mutator CustomMutator

访问器

可以使用访问器方法转换存储过程返回的数据。

namespace App\DataEntities;

use Carbon\Carbon;use DataEntities\DataEntity;
use BitMx\DataEntities\Enums\Method;
use BitMx\DataEntities\Enums\ResponseType;

class GetAllPostsDataEntity extends DataEntity
{
    ...
    
    #[\Override]
    public function defaultParameters(): array
    {
        return [
            'date' => Carbon::now(),
        ];
    } 
    
     /**
     * @return array<string, string>
     */
     #[\Override]
    protected function accessors(): array
    {
        return [
            'contact_id' => 'integer',
        ];
    }
}

这将在返回数据之前将contact_id键转换为整数。

可用的访问器

  • datetime: : 将值转换为DateTime实例。

  • datetime_immutable: : 将值转换为DateTimeImmutable实例。

  • bool: : 将值转换为布尔值 示例:如果值为1,则转换为true

  • int: : 将值转换为整数。

  • float: : 将值转换为浮点数

  • 字符串: : 将值转换为字符串。

  • array: : 将值从JSON字符串转换为数组。

    • object: : 将值从JSON字符串转换为对象。
  • collection: : 将值从JSON字符串转换为Laravel Collection。

自定义访问器

可以通过实现Accessable接口来创建自定义访问器。

namespace BitMx\DataEntities\Accessors;

use BitMx\DataEntities\Contracts\Accessable;

class CustomAccessor implements Accessable
{
    /**
     * {@inheritDoc}
     */
    public function get(string $key, mixed $value, array $data): mixed
    {
        
    }
}

可以使用Artisan命令创建新的访问器。

php artisan make:data-entity-accessor CustomAccessor

响应有用方法

响应对象有一些有用的方法来处理存储过程返回的数据。

data

data方法返回存储过程返回的数据作为数组。

$data = $response->data();

带键的数据

可以使用键获取数据

$data = $response->data('key');

带键和默认值的数据

可以使用键和默认值获取数据

$data = $response
    ->data('key', 'default value');

添加数据值

可以向数据数组添加值

$response->addData('key', 'value');

可以向数据数组添加数组

$response->addData(['key' => 'value']);

合并数据

可以将数组与数据数组合并

$response->mergeData(['key' => 'value']);

作为对象

可以获取数据作为对象

$data = $response->object();

作为集合

可以获取数据作为集合

$data = $response->collect();

成功

success方法在存储过程执行成功时返回true,否则返回false。

if ($response->success()) {
    // The stored procedure was executed successfully
} else {
    // There was an error executing the stored procedure
}

失败

fail方法在存储过程失败时返回true,否则返回false。

if ($response->failed()) {
    // There was an error executing the stored procedure
} else {
    // The stored procedure was executed successfully
}

抛出

默认情况下,响应对象在存储过程失败时不会抛出异常。您可以使用throw方法手动抛出异常。

$response->throw();

引导

可以使用boot方法在存储过程执行前后执行代码。

namespace App\DataEntities;

use BitMx\DataEntities\PendingQuery;
use DataEntities\DataEntity;
use BitMx\DataEntities\Enums\Method;
use BitMx\DataEntities\Enums\ResponseType;
use BitMx\DataEntities\Responses\Response;
use Illuminate\Support\Collection;


class GetAllPostsDataEntity extends DataEntity
{
    protected ?ResponseType $responseType = ResponseType::SINGLE;
    
    ...
    
    #[\Override]
    public function boot(PendingQuery $pendingQuery): void
    {
        $pendingQuery->parameters()->all('tag', 'laravel');
    }
    
}

特性

您可以使用特性向您的数据实体添加功能。将bootTrait方法添加到数据实体中以使用特性。

trait Taggable
{
    public function bootTaggable(PendingQuery $pendingQuery): void
    {
        $pendingQuery->parameters()->add('tag', 'laravel');
    }
}

bootTaggable方法将在执行存储过程之前被调用。

中间件

您可以使用中间件在执行存储过程前后执行代码。

namespace App\DataEntities;

use BitMx\DataEntities\PendingQuery;
use DataEntities\DataEntity;
use BitMx\DataEntities\Enums\ResponseType;
use BitMx\DataEntities\Responses\Response;
use Illuminate\Support\Collection;


class GetAllPostsDataEntity extends DataEntity
{
    protected ?ResponseType $responseType = ResponseType::SINGLE;
    
    ...
    
    #[\Override]
    public function boot(PendingQuery $pendingQuery): void
    {
        $pendingQuery->middleware()->onQuery(function (PendingQuery $pendingQuery) {
            $pendingQuery->parameters()->add('tag', 'laravel');
        });
        
        $pendingQuery->middleware()->onResponse(function (Response $response) {
            $response->addData('tag', 'laravel');
           
            return $response;
        });
    }
}

您还可以使用可调用的类作为中间件。此类应实现QueryMiddleware或ResponseMiddleware接口。

use BitMx\DataEntities\Contracts\QueryMiddleware;

class PageMiddleware implements QueryMiddleware
{
    public function __invoke(PendingQuery $pendingQuery): PendingQuery
    {
        $pendingQuery->parameters()->add('page', 1);
        
        return $pendingQuery;
    }
}
use BitMx\DataEntities\Contracts\ResponseMiddleware;
use BitMx\DataEntities\Responses\Response;

class TagMiddleware implements ResponseMiddleware
{
    public function __invoke(Response $pendingQuery): Response
    {
        $response->addData('tag', 'laravel');
        
        return $response;
    }
}
namespace App\DataEntities;

use BitMx\DataEntities\PendingQuery;
use DataEntities\DataEntity;
use BitMx\DataEntities\Enums\ResponseType;
use BitMx\DataEntities\Responses\Response;
use Illuminate\Support\Collection;


class GetAllPostsDataEntity extends DataEntity
{
    protected ?ResponseType $responseType = ResponseType::SINGLE;
    
    ...
    
    #[\Override]
    public function boot(PendingQuery $pendingQuery): void
    {
        $pendingQuery->middleware()->onQuery(new PageMiddleware());
        
        $pendingQuery->middleware()->onResponse(new TagMiddleware());
    }
}

插件

您可以使用插件向您的数据实体添加功能。

AlwaysThrowOnError

AlwaysThrowOnError插件将在存储过程失败时抛出异常。

namespace App\DataEntities;

use BitMx\DataEntities\PendingQuery;
use BitMx\DataEntities\Plugins\AlwaysThrowOnError;
use DataEntities\DataEntity;
use BitMx\DataEntities\Enums\Method;
use BitMx\DataEntities\Enums\ResponseType;
use BitMx\DataEntities\Responses\Response;
use Illuminate\Support\Collection;


class GetAllPostsDataEntity extends DataEntity
{
    use AlwaysThrowOnError;

    protected ?Method $method = Method::SELECT;
    
    protected ?ResponseType $responseType = ResponseType::SINGLE;
    
    ...
   
}

HasCache

HasCache插件将缓存存储过程返回的数据。

数据实体应实现Cacheable接口。

namespace App\DataEntities;

use BitMx\DataEntities\Contracts\Cacheable;
use BitMx\DataEntities\PendingQuery;
use BitMx\DataEntities\Plugins\AlwaysThrowOnError;
use BitMx\DataEntities\Plugins\HasCache;use DataEntities\DataEntity;
use BitMx\DataEntities\Enums\Method;
use BitMx\DataEntities\Enums\ResponseType;
use BitMx\DataEntities\Responses\Response;
use Illuminate\Support\Collection;


class GetAllPostsDataEntity extends DataEntity implements Cacheable
{
    use HasCache;

    protected ?ResponseType $responseType = ResponseType::SINGLE;
    
    ...
    
    public function cacheExpiresAt(): \DateTimeInterface {
        return now()->addMinutes(10);
    }
   
}

您可以使用invalidateCache方法清除缓存。

use App\DataEntities\GetPostDataEntity;

$dataEntity = new GetPostDataEntity(1);

$post = $response->invalidateCache();
$response = $dataEntity->execute();

或者,您可以使用disableCaching方法暂时禁用缓存。

use App\DataEntities\GetPostDataEntity;

$dataEntity = new GetPostDataEntity(1);

$post = $response->disableCaching();
$response = $dataEntity->execute();

响应对象将有一个isCached方法来检查数据是否被缓存。

use App\DataEntities\GetPostDataEntity;

$dataEntity = new GetPostDataEntity(1);

$post = $response->disableCaching();
$response = $dataEntity->execute();

$response->isCached(); //

数据传输对象

您可以使用数据传输对象(DTO)将存储过程返回的数据映射到PHP对象。

namespace App\Data;


class PostDat
{
    public function __construct(
        public int $id,
        public string $title,
        public string $content,
    ) 
    {
    
    }
}
namespace App\DataEntities;

use DataEntities\DataEntity;
use BitMx\DataEntities\Enums\Method;
use BitMx\DataEntities\Enums\ResponseType;
use BitMx\DataEntities\Responses\Response;
use Illuminate\Support\Collection;
use App\Data\PostData;

class GetPostDataEntity extends DataEntity
{
    protected ?Method $method = Method::SELECT;
    
    protected ?ResponseType $responseType = ResponseType::SINGLE;
    
    public function __construct(
        protected int $postId,
    ) 
    {
    
    }
    
    #[\Override]
    public function resolveStoreProcedure(): string
    {
        return 'spListPost';
    }

    #[\Override]
    public function defaultParameters(): array
    {
        return [
            'post_is' => $this->postId,
        ];
    } 
    
    public function createDtoFromResponse(Response $response): PostData
    {
        $data = $response->getData();
        
        return new PostData(
            id: $data['id'],
            title: $data['title'],
            content: $data['content'],
        );
    }
}

您可以使用dto方法从响应中获取DTO。

use App\DataEntities\GetPostDataEntity;

$dataEntity = new GetPostDataEntity(1);

$response = $dataEntity->execute();

/** @var PostData $post */
$post = $response->dto();

调试

您可以使用dd和ddRaw方法来调试发送到数据库的查询。

use App\DataEntities\GetPostDataEntity;

$dataEntity = new GetPostDataEntity(1);

$dataEntity->dd();

$dataEntity->ddRaw();

测试

您可以轻松地为数据实体创建集成测试。

模拟数据实体

您可以使用 DataEntity::fake 方法模拟数据实体。

use App\DataEntities\GetPostDataEntity;
use BitMx\DataEntities\DataEntity;
use BitMx\DataEntities\Responses\MockResponse;

it('should get the post', function () {
    DataEntity::fake([
        GetPostDataEntity::class => MockResponse::make([
            'id' => 1,
            'title' => 'Post title',
            'content' => 'Post content',
        ]),
    ]);

    $dataEntity = new GetPostDataEntity(1);

    $response = $dataEntity->execute();

    $post = $response->dto();

    expect($post->id)->toBe(1);
    expect($post->title)->toBe('Post title');
    expect($post->content)->toBe('Post content');
});

在使用 fake 方法时,execute 方法将返回 MockResponse::make 方法中指定的数据,而不会执行存储过程。

断言

您可以使用 assert 方法断言数据实体已被执行。

use App\DataEntities\GetPostDataEntity;
use BitMx\DataEntities\DataEntity;
use BitMx\DataEntities\Responses\MockResponse;

it('should get the post', function () {
    DataEntity::fake([
        GetPostDataEntity::class => MockResponse::make([
            'id' => 1,
            'title' => 'Post title',
            'content' => 'Post content',
        ]),
    ]);

    $dataEntity = new GetPostDataEntity(1);

    $response = $dataEntity->execute();

    $post = $response->dto();

    DataEntity::assertExecuted(GetPostDataEntity::class);
});

断言

您可以使用以下断言:

  • assertExecuted: 断言数据实体已被执行。
  • assertNotExecuted: 断言数据实体未被执行。
  • assertExecutedCount: 断言数据实体执行了特定次数。
  • assertExecutedOnce: 断言数据实体只执行了一次。

使用工厂

您可以使用工厂为数据实体创建模拟数据。

namespace Tests\DataEntityFactories;

use BitMx\DataEntities\Factories\DataEntityFactory;

class PostDataEntityFactory extends DataEntityFactory
{
    /**
     * {@inheritDoc}
     */
    public function definition(): array
    {
        return [
            'id' => $this->faker->unique()->randomNumber(),
            'title' => $this->faker->sentence(),
            'content' => $this->faker->paragraph(),
        ];
    }
}

要创建一个工厂,您应该扩展 DataEntityFactory 类并实现定义方法。

您可以使用 faker 属性生成模拟数据。

use App\DataEntities\GetPostDataEntity;
use Tests\DataEntityFactories\PostDataEntityFactory;
use BitMx\DataEntities\Responses\MockResponse;

it('should get the post', function () {
    $dataEntity = MockResponse::make(PostDataEntityFactory::new());

    $response = $dataEntity->execute();

    $post = $response->dto();

    expect($post->id)->toBe(1);
    expect($post->title)->toBe('Post title');
    expect($post->content)->toBe('Post content');
});

您可以直接将工厂传递给 MockResponse::make 方法。或者,您可以使用 create 方法创建一个数组。

use App\DataEntities\GetPostDataEntity;
use Tests\DataEntityFactories\PostDataEntityFactory;
use BitMx\DataEntities\Responses\MockResponse;

it('should get the post', function () {
    $dataEntity = MockResponse::make(PostDataEntityFactory::new()->create());

    $response = $dataEntity->execute();

    $post = $response->dto();

    expect($post->id)->toBe(1);
    expect($post->title)->toBe('Post title');
    expect($post->content)->toBe('Post content');
});

您还可以使用 count 方法创建一个模拟数据的数组。

use App\DataEntities\GetPostDataEntity;
use Tests\DataEntityFactories\PostDataEntityFactory;
use BitMx\DataEntities\Responses\MockResponse;

it('should get the post', function () {
    $dataEntity = MockResponse::make(PostDataEntityFactory::new()->count(10));

    $response = $dataEntity->execute();

    $posts = $response->dto();

    expect($posts)->toHaveCount(10);
});

您可以使用 state 方法更改工厂的默认值。

use App\DataEntities\GetPostDataEntity;
use Tests\DataEntityFactories\PostDataEntityFactory;
use BitMx\DataEntities\Responses\MockResponse;

it('should get the post', function () {
    $dataEntity = MockResponse::make(PostDataEntityFactory::new()->state([
        'title' => 'Custom title',
    ]));

    $response = $dataEntity->execute();

    $post = $response->dto();

    expect($post->title)->toBe('Custom title');
});

或者在工厂中创建一个新方法来更改默认值。

namespace Tests\DataEntityFactories;

use BitMx\DataEntities\Factories\DataEntityFactory;

class PostDataEntityFactory extends DataEntityFactory
{
    /**
     * {@inheritDoc}
     */
    public function definition(): array
    {
        return [
            'id' => $this->faker->unique()->randomNumber(),
            'title' => $this->faker->sentence(),
            'content' => $this->faker->paragraph(),
        ];
    }
    
    public function withPublishedDate(array $state): DataEntityFactory
    {
        return $this->state([
            'published_date' => now(),
        ]);
    }
}
use App\DataEntities\GetPostDataEntity;
use Tests\DataEntityFactories\PostDataEntityFactory;
use BitMx\DataEntities\Responses\MockResponse;

it('should get the post', function () {
    $dataEntity = MockResponse::make(PostDataEntityFactory::new()->withPublishedDate());

    $response = $dataEntity->execute();

    $post = $response->dto();

    expect($post->published_date)->toBe(now());
});

您可以使用异常创建一个模拟。

use App\DataEntities\GetPostDataEntity;
use Tests\DataEntityFactories\PostDataEntityFactory;
use BitMx\DataEntities\Responses\MockResponse;

it('should get the post', function () {
    $dataEntity = MockResponse::makeWithException(new \Exception('Error'));

    $response = $dataEntity->execute();
})
    ->throws(\Exception::class, 'Error');

响应类型

您可以使用 responseType 方法设置响应类型。

namespace Tests\DataEntityFactories;

use BitMx\DataEntities\Enums\ResponseType;use BitMx\DataEntities\Factories\DataEntityFactory;

class PostDataEntityFactory extends DataEntityFactory
{
    /**
     * {@inheritDoc}
     */
    public function definition(): array
    {
        return [
            'id' => $this->faker->unique()->randomNumber(),
            'title' => $this->faker->sentence(),
            'content' => $this->faker->paragraph(),
        ];
    }
    
    public function responseType() : ResponseType{
         return ResponseType::COLLECTION;
    }
}

您可以在 MockResponse 上更改响应类型。

use App\DataEntities\GetPostDataEntity;
use Tests\DataEntityFactories\PostDataEntityFactory;
use BitMx\DataEntities\Responses\MockResponse;

it('should get the post', function () {
    $dataEntity = MockResponse::make(PostDataEntityFactory::new()->asCollection());

    $response = $dataEntity->execute();

    ....

});

您可以使用 artisan 命令创建一个新的工厂。

php artisan make:data-entity-factory PostDataEntityFactory

此命令将在 tests/DataEntityFactories 目录中创建一个新的工厂。