fr8train/skeleton-ms 问题修复 & 功能扩展

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

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

fr8train/skeleton-ms

Composer 安装命令:

composer create-project fr8train/skeleton-ms

包简介

Skeleton MicroService powered by Slim4 Framework.

README 文档

README

PHP poweredby Bitbucket open issues

Skeleton MicroService powered by Slim4 Framework.

Installation

Use Composer to install SkeletonMS locally.

composer create-project fr8train/skeleton-ms [directory] [version] 

If you want to include the git link to send updates to the repository, use:

composer create-project --keep-vcs fr8train/skeleton-ms [directory] [version]

Post-Installation

Please create a .env file at the root directory of the project.

If you would like to use the provided Database Singleton for MySQL, please add these to your .env file:

MYSQL_HOST=""
MYSQL_DATABASE=""
MYSQL_USERNAME=""
MYSQL_PASSWORD=""

Seriously, I just ran into this again. Without at least a blank .env file, you will receive a HTTP 500 error. A blank one is required for the project to run, and we recommend that you use it as the location to store any sensitive information such as passwords or keys. To retrieve any data stored here, use vlucas/phpdotenv.

If for some reason you get an error message indicating that a class in the code cannot be found, first try to reload the autoloaded classes through Composer.

composer dump-autoload

Useful Design Practices and Libraries

ramsey/uuid - PHP UUIDv7 libray

This was added to our project to increase security, as working with UUIDs continues to rise and leave predictable int unsigned IDs behind. We're using specifically his UUIDv7 object as v7 UUIDs are universally unique lexicographically-sortable identifiers meaning they're UUIDs that can be sorted and searched in databases in meaningful ways (see his documentation here). Here's the main link to his documentation.

Here is an example on how to use it with IDs set up in MySQL 8 using a binary(16) column.

/* 
 *  FOR AN OBJECT USING RAMSEY'S UUIDv7 CLASS
 *  YOU CAN USE THE FOLLOWING TO GET THE BINARY ID
*/
    
$id = Ramsey\Uuid\Uuid::uuid7()->getBytes();

/* 
 *  FOR A RAW BINARY ID, USE THE FOLLOWING TO GET THE UUID OBJECT
 *  ALSO AVAILABLE FOR STRINGS AS WELL
*/

$id = Ramsey\Uuid\Uuid::fromBytes($id);
$id = Ramsey\Uuid\Uuid::fromString($id);

/*
 *  FOR ANY SORT OF STRING OUTPUT, SUCH AS JSON SERIALIZATION,
 *  USE THE FOLLOWING TO GET THE STRING ID
 */

$id = \Ramsey\Uuid\Uuid::uuid7()->toString();

We also updated the ObjectFactory::loadClass() method so that you can also auto detect byte strings and attempt to translate them back into UUIDs.

$lists = $this->db->execQueryString("SELECT * FROM lists")
            ->fetchAll(PDO::FETCH_ASSOC);

$lists = array_map(fn(array $el): ListClass => ObjectFactory::loadClass(ListClass::class, $el), $lists);

sorskod/db PDO wrapper

This is a great little library for making DB work a lot simpler to manage. Information and practices can be found at his Github Page or his similar Packgist. One thing that isn't implicit in his documentation to note, if you would like to bind your parameters for SQL Injection scrubbing please follow this example:

// IF YOU HAVE BOOLEAN VALUES, DON'T FORGET TO CAST THEM TO INTs IN PHP
// OR IT WILL THROW AN ERROR

$stmt = $this->db->execQueryString("select * from users")]);
$users = $stmt->fetchAll(PDO::FETCH_ASSOC);

Insertable

To ease the process of dealing with the UUIDs conversion prior to DB inserts, we also came up with the UuidInsertableInterface, which requires a toInsertable() method to be created on the class.

class Item implements UuidInsertableInterface
{
    public UuidInterface $id;
    public UuidInterface $list_id;
    ...
    
    public function toInsertable(): array 
    {
        return [
            'id' => $this->id->getBytes(),
            'list_id' => $this->list_id->getBytes(),
            'name' => $this->name,
            'created_at' => $this->created_at,
            'completed_at' => $this->completed_at,
        ];
    }
}

...

$this->db->insert("lists", $list->toInsertable());

...

Updatable

Updatables is a feature that allows for a property-by-property distinction of which fields should be selected for (and against) creating an array of key/values to pass to the update method from sorskod's DB. It also pairs with the UpdatableTrait.

class Item
{
    use UpdatableTrait;
    
    // DON'T WANT TO UPDATE THE ID
    public UuidInterface $id;
    // DO WANT TO UPDATE NAME AND IS_MINE
    #[Updatable]
    public string $name;
    #[Required, Updatable]
    public bool $is_mine;
    #[Updatable(UpdatableTypes::PASSWORD)]
    public ?string $password;
}

...

$this->db->update("items", $item->toUpdatable(), "id = ?", [
    $item->id->getBytes()
]);

...

Updatables also now have types, in order to support hashing passwords for example on an update to a User. We also added synergetic logic between Updatable and Required if you set toUpdatables() to strict mode.

UpdatableTypes and Enums

We have several UpdatableTypes: PASSWORD, CHILD, and DEFAULT.

DEFAULT is your typical straight-across key/value assignment.

CHILD is a special type that will indicate that the value is a child or parent Model and should be treated as another entity entirely.

PASSWORD is a special type that will hash the value before assigning it back to the key.

Encryptable

We decided to handle encryption and decryption for you. Simply add the Encryptable attribute to any Model and feel free to use the encrypt() and decrypt() methods as needed.

In order for this feature to work, you must set the ENCRYPTION_KEY environment variable to a 32-byte (256-bit) hexadecimal string. This key is used for encryption and decryption operations. If the key is not set, an exception will be thrown. You create this key by running the following command in any PHP environment (e.g. php -a in your terminal):

bin2hex(random_bytes(32));

CastableTrait

The Castable Trait provides a quick assignment to your PHP custom objects or classes as long as you remember to include default values in the constructor of your custom object or class.

// CUSTOM CLASS DEFINITION
class ServiceResponse
{
    use CastableTrait;

    public int $http_code;
    public string $message;
    public mixed $payload;
    public ?Throwable $error;

    public function __construct(int       $http_code = 200,
                                string    $message = '',
                                mixed     $payload = null,
                                ?Throwable $error = null)
    {
        $this->http_code = $http_code;
        $this->message = $message;
        $this->payload = $payload;
        $this->error = $error;
    }
}

// ACTUAL CASTING
// BECAUSE OF DEFAULT VALUES ANY ONE OF THESE PROPERTIES CAN ACTUALLY BE SKIPPED IN DECLARATION
return ServiceResponse::cast([
    'http_code' => 500,
    'message' => 'Internal Server Error',
    'payload' => [
        'trace' => $exception->getTrace()
    ],
    'error' => $exception // instanceof Exception, Throwable, or null
]);

Validation

We recently added both the Required attribute and the ValidatableTrait to our project.

The Required attribute is a simple way to ensure that a property has to have a truthy value (we're using empty() to determine this) when attempting to communicate with our API. Simply add it to a property to ensure that when the ->validate() call is made, we will determine whether or not it is 'empty'.

class Foo {
    use ValidatableTrait;
    
    #[Required]
    public string $bar;
}

The ValidatableTrait provides the methodology for validating the incoming data as well as formulating the response in a single location.

public function test(Foo $foo): ServiceResponse {
    $errors = $foo->validate();
    if (!empty($errors)) {
        // RETURNS A HTTP 422, MISSING REQUIRED PARAMETERS
        return $foo->formatServiceResponse($errors);
    }
    
    return ServiceResponse::cast([
        'message' => 'I made it past the validator!'
    ])  
}

ServiceResponse Design

Implementing Services by always returning ServiceResponses can prove to make your code much easier to navigate and reduce bugs by following the design:

<?php

namespace services;

use models\HelloWorld;
use models\ServiceResponse;

class HelloWorldService
{
    public static function hello(HelloWorld $world) :  ServiceResponse
    {
        /*
         *  DO YOUR SERVICE LOGIC
         */

        // ANY OF THESE PROPERTIES ON SERVICE RESPONSE
        // CAN TECHNICALLY BE BLANK BECAUSE OF DEFAULT VALUES
        if (isset($exception)) {
            return ServiceResponse::cast([
                'http_code' => 500,
                'message' => 'Internal Server Error',
                'payload' => [
                    'trace' => $exception->getTrace()
                ],
                'exception' => $exception 
            ]);
        }

        return ServiceResponse::cast([
            'message' => $world->message,
            'payload' => [
                'Foo' => 'Bar'
            ]
        ]);
    }
}

See how handling the ServiceResponse makes Controller handling much easier (note: we also updated the BaseController class to simplify handling the responses):

$serviceResponse = HelloWorldService::hello(ObjectFactory::loadClass(HelloWorld::class, $data));

return $this->handle($response, $this->log, $serviceResponse);

In addition, it also increases DRY concepts when dealing with nested objects. For example, with the REST URL POST https://mysite.com/foo/1/bar checking for the existence of foo with ID=1 becomes a snap: We also added ->isError() and ->isSuccess() methods to the ServiceResponse class, since successful responses are all 2xx errors, not just 200.

public function create(Foo $foo, Bar $bar): ServiceResponse
{
     // DOES FOO EXIST?
     $serviceResponse = $this->fooService->fetch($foo);
     // IF NOT, RETURN HTTP 404 WRAPPED IN A SERVICERESPONSE OBJECT
     if ($serviceResponse->isError()) return $serviceResponse;

     $errors = $bar->validate();
     if (!empty($errors)) {
         return $bar->formatServiceResponse($errors);
     }

     $this->db->insert("bar", $bar->toInsertable());

     return ServiceResponse::cast([
         'payload' => $bar
     ]);
}

Null-Instantiation Design Pattern

By design, the system is built around the concept of "you expect it, it will be there" style of coding. All models are considered open, with public properties as placeholders in order to remove any doubt as to what properties exist on any of the custom classes in our project. This helps to align database fields as well as expected API parameters. It is also key to our ObjectFactory::loadClass(), which uses Reflection to determine what data existing on the source has identical fields on the target to which it can map the values. A lot of words to say, you don't need to isset($foo->bar) before checking for the value on an object ever again.

Singleton Design Pattern

We added the Singleton Design Pattern into our project because it became apparent that you can easily create way too many Database connection objects. Why bother when you can just pass around one? Relying on the SingletonInterface, custom singletons are very straight-forward to set up. You are required to:

  • implement the Singleton Interface
  • instantiate your object statically
  • create a private static instance of this object
  • create a private constructor to force "static"-ness
  • and finally, create a method to return the existing instance
class YourObjectSingleton implements SingletonInterface
{
    private static ?YourObject $instance = null;
    
    private function () {}
    
    public static function instantiate(): object
    {
        // INCLUDE ANY PARAMETERS YOU MIGHT NEED 
        // TO INJECT INTO YOUR CONSTRUCTOR
        return new YourObject();
    }
    
    public static function getInstance(): object
    {
        return self::$instance ??= self::instantiate();
    }
}

...

use YourObjectSingleton as yos;

// WHERE FOO() EXISTS ON YourObject
$bar = yos::getInstance()->foo();

...

Contributing

Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.

Please make sure to update tests as appropriate.

License

MIT

fr8train/skeleton-ms 适用场景与选型建议

fr8train/skeleton-ms 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 42 次下载、GitHub Stars 达 0, 最近一次更新时间为 2021 年 12 月 24 日, 在 PHP 生态内属于活跃度较高的组件。

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

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

围绕 fr8train/skeleton-ms 我们能提供哪些服务?
定制开发 / 二次开发

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

BUG 修复 & 性能优化

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

项目外包 & 长期维护

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

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

统计信息

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

GitHub 信息

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

其他信息

  • 授权协议: MIT
  • 更新时间: 2021-12-24