neoan.io/core

Neoan.io Lenkrad 框架


README

Build Coverage Maintainability php vegan

在线文档 @ lenkrad.neoan3.rocks

这是 neoan 的核心重写版本,目前不适用于生产环境。使用现代 PHP,我们相信可以创建简单且静态的内部 API,同时避免了通常与模拟、注入和可测试性相关的问题。

它很现代!

快速浏览

// A MODEL
<?php
...
class User extends Model 
{
    
    #[IsPrimary]
    public readonly $id;
    
    public ?string $name = null;
    
    #[IsUnique]
    public string $email;
    
    public string $job = 'employee';
    
    #[Transform(Hash::class)]
    public string $password;
    
    use TimeStamps;
    use Setter;
}
// A CONTROLLER
<?php
...
$user = new User([
    'email'=> 'some@mail.com', 
    'name' => 'Someone',
    'password' => '123123'
]);
// reconsider name?
$user->name = 'Adam';
$user->store();

...
// or e.g. when updating a password

$user = User::retrieveOne([
            'email' => 'some@email.com'
        ]);

// Don't worry! Hashing for this property 
// is always ensured by the model
[ 
    'newPassword' => $user->password 
] = Request::getInputs();
$user->store();

正如你所见,在保持易于理解的风格的同时,可以省去很多冗余。

运行此程序需要 PHP 8.1 和 composer2

为什么?

PHP 已经走了很长的路。大多数框架都关注向后兼容性,以便允许现有的代码库安全地更新框架安全补丁,而不会出现破坏性变化。然而,如果你正在启动一个项目,你为什么要放弃现代 PHP 的力量呢?

你意识到框架如果能利用

  • 属性
  • 枚举
  • 交叉类型
  • 匹配表达式
  • 命名参数
  • 只读属性
  • 联合类型
  • ...

执行时间将会更快,但这能否使你的生活更轻松?

入门指南

composer require neoan.io/core

index.php

<?php

use Neoan\NeoanApp;
use Neoan\src\Routing\Route;

require_once 'vendor/autoload.php';
$app = new NeoanApp(__DIR__, __DIR__);

Route::get('/hello-world')
    ->inject(['msg' => 'Hello World']);

$app->run();

php -S localhost:8080 index.php

设置

即将推出:正在创建项目脚本

此 README 将指导你了解个人需求。对于急于求成的人 - 以及作为速查表,找到一个基本的设置脚本

composer require neoan.io/core

composer require neoan.io/legacy-db-adapter(可选:在开发过程中,您也可以使用 Neoan\Database\SqLiteAdapter)

您可以选择自己的文件夹结构。目前,我们将假设以下结构

project
+-- public
|   +-- index.php
+-- src
|    +-- Attributes
|    +-- Cli
|    +-- Config
|    |    +-- Setup.php
|    +-- Controllers
|    +-- Middleware
|    +-- Models
|    +-- Routes
|    |   +-- HtmlRoutes.php
|    +-- Views
|        +-- main.html
|        +-- home.html
+-- vendor
+-- cli
+-- composer.json

composer.json 中利用以下 PSR 命名空间定义

"autoload": {
    "psr-4": {
      "App\\": "src/"
    }
  }

/cli

#!/usr/bin/env php
<?php
// the first line is necessary if we don't use the extension ".php"!
// this file load our cli capabilities and is exposed to
// allow advanced users to integrate own commands (based on symfony console)

use App\Config\Config;
use Neoan\Cli\Application;
use Neoan\NeoanApp;


require_once 'vendor/autoload.php';

$app = new NeoanApp(__DIR__, __DIR__);
new Config($app);
$console = new Application($app);
$console->run();

/public/index.php

use App\Config\Setup;
use App\Routes\HtmlRoutes;
use Neoan\NeoanApp;

require_once dirname(__DIR__) . '/vendor/autoload.php';

$srcPath = dirname(__DIR__) . '/src';
$publicPath = __DIR__; // where this very script runs

$app =  new NeoanApp($srcPath, $publicPath);
new Setup();
new HtmlRoutes();
$app->run();

/src/Routes/HtmlRoutes.php

namespace App\Routes;


class HtmlRoutes {
    function __construct()
    {
        Routes::get('/')->view('home.html');
    }
}

/src/Config/Setup.php

namespace App\Config;

use Neoan\Database\Database;
use NeoanIo\MarketPlace\DatabaseAdapter;
use Neoan\Helper\Env;
use Neoan\Response\Response;
use Neoan\Render\Renderer;

class Setup {
    function __construct()
    {
        // Database setup
        $dbClient = [
            'host' => Env::get('DB_HOST', 'localhost'),
            'name' => Env::get('DB_NAME', 'neoan_io'),
            'port' => Env::get('DB_PORT', 3306),
            'user' => Env::get('DB_USER', 'root'),
            'password' => Env::get('DB_PASSWORD', ''),
            'charset' => Env::get('DB_CHARSET', 'utf8mb4'),
            'casing' => Env::get('DB_CASING', 'camel'),
            'assumes_uuid' => Env::get('DB_UUID', false)
        ];
        Database::connect(new DatabaseAdapter($dbClient));
        
        // Defaults
        Response::setDefaultOutput(ResponseOutput::HTML);
        Renderer::setTemplatePath('src/Views');
    
    }
}

路由

注册路由既简单又直观

use Neoan\Routing\Route;

Route::request(string $httpMethod, string $endpoint, Routable ...$classes);

HTTP 方法

只需使用 method 关键字以简短语法注册路由。

以下方法已实现

  • get
  • post
  • put
  • patch
  • delete

示例

use Neoan\Routing\Route;

Route::get(string $endpoint, Routable ...$classes);

端点参数

端点可以处理冒号符号的参数,如下所示

use Neoan\Routing\Route;

Route::get('/users/:id')
    ...

这将匹配对 "/users/12" 的调用,并将值(此处为 "12")暴露给请求(请参阅 处理输入和输出

可路由类

您可以将任意数量的类链接到路由(中间件)中。类 必须 实现 Routable 接口和 invoke 函数,并返回以下类型之一

  • 数组
  • 字符串
  • Neoan\Model\Collection
  • 模型 | Neoan\Model\Model

示例

namespace App\Controllers;

use Neoan\Routing\Routable;

class Controller implements Routable
{

    public function __invoke(array $provided): array
    {
        return ["msg" => "Hello World"];
    }
}
use Neoan\Routing\Route;

Route::get('/', App\Controllers\Controller::class)

链式路由

之前执行类的返回值将被暴露给下一个类。

假设以下中间件

namespace App\Middleware;

use Neoan\Errors\Unauthorized;
use Neoan\Routing\Routable;
use Neoan3\Apps\Stateless;

class NeedsAuth implements Routable
{
    public function __invoke(array $provided = []): array
    {
        try{
            return ['auth' => Stateless::validate()];
        } catch (\Exception $e) {
            new Unauthorized();
        }
    }
}

使用以下路由

use Neoan\Routing\Route;
use App\Middleware\NeedsAuth;
use App\Controllers\Controller;

Route::get('/', NeedsAuth::class, Controller::class)

在这种情况下,返回的数组被传递到我们的控制器类中,覆盖了之前可能设置的“auth”的值。如果授权失败,控制器永远不会被执行,因为“未授权”错误终止了执行。然而,如果我们被授权,我们现在可以使用“auth”了。

namespace App\Controllers;

use Neoan\Routing\Routable;

class Controller implements Routable
{

    public function __invoke(array $provided): array
    {
        ['auth' => $auth] = $provided;
        // better not do that?
        return ["token-payload" => $auth];
    }
}

响应处理程序

默认情况下,路由解析为具有内置的JSON响应。但是,您可以更改默认行为

use Neoan\Response\Response;
use Neoan\Enums\ResponseOutput;
Response::setDefaultOutput(ResponseOutput::HTML)

或者使用特定路由的输出处理程序

use Neoan\Routing\Route;
use Neoan\Response\Response;
use App\Controllers\Controller;

Route::get('/', Controller::class)->response([Response::class,'html']);
// or whatever handler you want:
Route::get('/my-handler', Controller::class)->response([App\Own\MyResponseHandler::class,'answerMethod'])

注入

为了简化生活,您可以直接将值注入到路由中

use Neoan\Routing\Route;
use App\Controllers\Controller;

Route::get('/', Controller::class)->inject(['title'=>'my_app']);

这将向所有列出的类提供值,并经常可以替代中间件。

视图

默认模板引擎是“neoan3-apps/template”,这是一个经过实战考验的解决方案。要设置视图的位置,首先定义通用目录。例如:

use Neoan\Render\Renderer;

Renderer::setTemplatePath('src/Views');

然后,您可以在路由时使用相对路径

use Neoan\Routing\Route;

Route::get('/')
    ->response([Response::class,'html'])
    ->inject(['user' => ['firstName' => 'Sam']])
    ->view('/home.html');
<!-- home.html -->
<p>{{user.firstName}}</p>

有关在neoan.io lenkrad中关于模板化的更多信息,请参阅neoan.io lenkrad的模板化neoan3-apps/template模板引擎

路由属性

您还可以使用属性注册路由。为了做到这一点,必须满足两个先决条件

  1. 通过属性路由注册命名空间
  2. 向可路由类添加适当的属性
// e.g. in idex.php
// ...
$app = new NeoanApp( dirname(__DIR__), __DIR__, dirname(__DIR__));

// invoke using the namespace of whereever your routables are located
$app->invoke(new Neoan\Routing\AttributeRouting('Controller'));
// e.g. Controller\WebRoute.php
namespace Controller;

#[Web('/','/test.html')]
class WebRoute implements Neoan\Routing\Routable
{
    public function __invoke(array $provided): array
    {
        return ["msg" => "Hello World"];
    }
}

这将以类似Ruby的方式注册路由,而不是需要手动定义路由。以下属性可供您使用:

  • Web(string $route, string $viewTemplate, ...$middlewareClasses)
  • Get(string $route, ...$middlewareClasses)
  • Post(string $route, ...$middlewareClasses)
  • Put(string $route, ...$middlewareClasses)
  • Patch(string $route, ...$middlewareClasses)
  • Delete(string $route, ...$middlewareClasses)

处理输入与输出

输入处理非常直观。以下是一些方便的“Request”类方法:

  • getInput(string $name): string
  • getInputs(): array
  • getQuery(string $name): string
  • getQueries(): array
  • getParameter(string $name): string
  • getParameters(): array
Route::get('/api/users/:id', UserShowController::class);
Route::post('/api/user', UserCreateController::class);

UserShowController

// call: GET:/api/users/1?some=value
...
public function __invoke(array $provided): array
    {
        return [
            'queryValues' => Request::getQueries(), // outputs ['some' => 'value']
            'userId' => Request::getParameter('id') // outputs "1"
        ];
    }
...

UserCreateController

// call: POST:/api/user payload: {"userName":"Tobi"}
...
public function __invoke(array $provided): array
    {
        ['userName' => $userName] = Request::getInputs();
        return [
            'user' => $userName // outputs "Tobi"
        ];
    }
...

模板

默认模板引擎附加到默认渲染器。两者都可以交换,但现在让我们关注内置工具。

这里只介绍模板引擎的基础知识,有关更深入的了解,请参阅neoan3-apps/template仓库

要设置默认模板路径,请使用

use Neoan\Render\Renderer;

Renderer::setTemplatePath(string $path);

注意:默认情况下,模板引擎使用项目路径。使用“setTemplatePath”会覆盖该值。这意味着您必须声明相对于项目根的路径。让我们看看设置示例

/public_html 
  /index.php    
/src
  /Models
  /Views      <- This is where we want to store our views
  /Controller
/vendor       <- Hint: always define from the "vendor" folder's parent on
...           

在上面的场景中,设置我们的模板路径将是

use Neoan\Render\Renderer;

Renderer::setTemplatePath('src/Views');

骨架

use Neoan\Render\Renderer;

Renderer::setHtmlSkeleton(string $templatePath, string $routePlacement, array $renderVariables)

为了简化最常见的场景,渲染器使用一个“骨架”来包围您特定的视图。这个骨架可以被视为一个外壳或框架,通常包括页眉和页脚。

示例

<!-- /src/Views/main.html -->

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="/css/style.css">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>{{title}}</title>
</head>
<body>
<header>
    <nav><!--...--></nav>
</header>
{{routePlacement}}
</body>
</html>

现在我们可以将此文件设置为我们的骨架

use Neoan\Render\Renderer;
use Neoan\Store\Store;

Renderer::setHtmlSkeleton('src/Views/main.html','routePlacement',[
    'title' => Store::dynamic('title'), // 'title' isn't set at this point, so we use the dynamic store
    'webPath' => $app->webPath          // neoan instance relative webPath in case we need it
])

为了完成示例,我们将创建一个视图和路由

<!-- /src/Views/you.html -->

<p>I am here with <strong>{{you}}</strong></p>
use Neoan\Routing\Route;
use Neoan\Response\Response;
use Neoan\Enums\ResponseOutput;
use App\YouClass;

Response::setDefaultOutput(ResponseOutput::HTML);
Route::get('/test/:you', YouClass::class)->view('/you.html');
use Neoan\Store\Store;
use Neoan\Routing\Routable;
use Neoan\Request\Request;

class YouClass implements Routable{

    public function __invoke(Injections $provided): array
    {
        Store::write('title','you-route');  // write to dynamic store
        return Request::getParams();        // we know this includes "you"
    }

}

访问/test/Eve时的输出将是

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="/css/style.css">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>you-route</title>
</head>
<body>
<header>
    <nav><!--...--></nav>
</header>
<p>I am here with <strong>Eve</strong></p>
</body>
</html>

模板基础知识

您已经看到了带有大括号{{var}}的通用标记。以下是一些针对常见任务的提示,假设以下PHP输出

 ...
 return [
    'deep' => [
        'key' => 'one'
    ],
    'iterateMe' => [
        ['name' => 'Sam'],
        ['name' => 'Adam']
    ]
 ];
 ...

嵌套变量

迭代

条件

事件

事件是有用的工具,可以控制和抽象逻辑,并提供添加自定义功能的清洁方式。核心本身使用系统事件(GenericEvents)进行调试和测试,因此在运行时监听或调度它们不会产生副作用

  • DATABASE_ADAPTER_CONNECTED
  • BEFORE_DATABASE_TRANSACTION
  • AFTER_DATABASE_TRANSACTION
  • BEFORE_RENDERING
  • 请求处理初始化
  • 请求头设置
  • 请求输入解析
  • 路由处理器初始化
  • 路由已注册
  • 响应处理器设置
  • 响应之前
  • 路由注入
  • 路由执行前

我们使用通用的术语来命名方法

use Neoan\Event\Event;

Event::on('log', function($event){
    $data = [
        'time' => time(),
        'event' => $event
    ];
    file_put_contents(dirname(__DIR__,2) . '/log.txt', json_encode($data), FILE_APPEND);
});

...
// somewhere else
try{
    ...code
} catch(\Exception $e){
    Event::dispatch('log', $e->getMessage());
}

除了这个功能之外,您还可以通过使用 Event::subscribeToClass(string $className, callable $closureOrInvokable) 来监听 Routable 和 Model 类触发的通知。

如果您想扩展可以监听的类,只需以这种方式实现 Neoan\Event\Listenable

use Neoan\Event\Event;
use Neoan\Event\Listenable;
use Neoan\Event\EventNotification;

class AnyClass implements Listenable
{
    private EventNotification $notifier;
    function __construct()
    {
        $this->notifier = Event::makeListenable($this);
    }
    function doSomething(string $value)
    {
        ...
        $this->notifier->inform($value);
    }
}

这在链接中间件并希望对尚未发生的输出做出反应时特别有用。

动态存储

静态存储对象是设计决策的一部分。它作为一个“对所有人开放”的全局内存,由核心本身使用,并且完全暴露给用户空间。它的一项核心能力是能够在没有事件监听器的语法开销的情况下使用尚未初始化的值。

use Neoan\Store\Store;
$totalRuntime = Store::dynamic('totalRuntime');
$start = time();
for($i = 0; $i <2; $i++){
    echo $totalRuntime; // first iteration: null, second iteration: ~ 1
    sleep(1);
    Store::write('totalRuntime', time() - $start);
}
echo $totalRuntime; // ~ 2

在实践中,这允许我们使用最终将设置的变量在代码中使用,创建一个类似承诺的结构,而不需要实际异步行为。

模型

现代 MVC 框架使用对象关系映射(ORM)与数据交互。虽然 neoan.io lenkrad 没有不同,但 PHP 的可能性终于增长到不需要手动映射数据库结构和运行时对象的程度,前提是正确执行。

数据库设置

此包尚未附带默认数据库适配器。目前,mysql 和 mariadb 连接是通过 neoan.io/legacy-db-adapter 包创建的。

composer require neoan.io/legacy-db-adapter

请参阅此说明中的 设置neoan3-apps/db 了解设置说明和更深入的了解。

模型基础知识

在核心上,模型是一个继承核心模型类能力的单个对象。

示例

namespace App\Models;

use Neoan\Model\Model;
use Neoan\Helper\DateHelper;
use Neoan\Model\Attributes\Initialize;
use Neoan\Model\Attributes\IsPrimaryKey;
use Neoan\Model\Attributes\IsUnique;
use Neoan\Model\Attributes\Ignore;
use Neoan\Model\Attributes\Type;
use Neoan\Model\Collection;
use Neoan\Model\Traits\TimeStamps;

class MovieModel extends Model {
    // primary keys can either be UUIDS or auto-incremented integers
    // as our database setup refused the assumption of UUIDS, integers it is! 
    // every model needs a primary key, which is indicated by the attribute "IsPrimaryKey"
    
    #[IsPrimaryKey]
    public int $id;
    
    // Can there be two movies with the same name? Let's decide no:
    // The "IsUnique" attribute let's the auto-migration know that we are serious about this decision.
    
    #[IsUnique]
    public string $name;
    
    // Let's go crazy: What if wanted a type that cannot be inferred as it isn't built in?
    // We are going to need to worry about two things: 
    // First, the database type shouldn't default to string (or varchar, in our case), 
    // so we define it using the "Type" attribute
    // Additionally, we would like our model to assume the current date when a model is created,
    // so we initialize a Datehelper instance on creation.
    
    #[
        Type('date',null),
        Initialize(new DateHelper())
    ]
    public string $releaseDate; 
    
    // Just to lighten up the attribute-overload, let's create a regular field
    // Since it has the type string it will default to a (short-)string data type (e.g. varchar(255)
    
    public string $studio;
    
    // What about relations?
    // there is more than one review for a given movie, so we attach ReviewModel instances in a
    // collection (see Collections) to the property $reviews based on the ReviewModel's foreign key
    // "movieId" which points to our primary key "id"
    
    #[HasMany(ReviewModel::class, ['movieId' => 'id'])]
    public Collection $reviews;
    
    // I don't know what I need it for, but the following property is ignored by database transactions
    // and only serves for us to store values.
    
    #[Ignore]
    public string $aProperty = 'new';
    
    // Traits can be useful to fight repetition. This packaged trait delivers us the properties
    // - createdAt (a timestamp filled at creation of the Model)
    // - updatedAt (a timestamp that is filled whenever a Model is stored to the database) and
    // - deletedAt (a timestamp allowing soft deletion)
    use TimeStamps;
}    

为了完整性,我们的 ReviewModel 将看起来像这样

namespace App\Models;

Neoan\Model\Traits\Setter;
Neoan\Model\Model;
use Neoan\Model\Attributes\IsPrimaryKey;
use Neoan\Model\Attributes\IsForeignKey;
use Neoan\Model\Traits\TimeStamps;

class ReviewModel extends Model{
    
    // Young devs in your team?
    // It's probably smart to set the primary key to "readonly" to protect your padawans
    // from stupid ideas. However, this requires the model itself to initialize the 
    // property after database hydration. To automate this process, use the trait "Setter"
    
    use Setter;
    
    #[IsPrimaryKey]
    public readonly int $id;
    
    // Who cares about critics?
    // Let's make this field nullable
    
    public ?string $author = null;
    
    // We are using the attribute "Type" again.
    // this time, we skip the length but nclude a default
    
    #[Type('MEDIUMTEXT', null, 'Awesome')]
    public string $content;
    
    // Remember our model "Movie"? 
    // While we don't need to declare this as foreign key,
    // we might want to speed up database queries once our cinema bursts with visitors
    
    #[IsForeignKey]
    public int $movieId;
    
    use TimeStamps;
    
    // Want to make your despise for critics known to whoever has to write raw
    // queries? Name your table however you like instead of being base on the model name.
    
    const tableName = 'ticks';
    
}

我们将跳过一些步骤,以便实际使此示例工作

php cli migrate:mysql App\Model\MovieModel & php cli migrate:mysql App\Model\ReviewModel

创建

要创建新记录,只需存储模型的一个实例。

...
// either initialte with an assoc array
$movie = new MovieModel([
    'name' => 'The Matrix'  
]);

// or set the individual property
$movie->studio = 'Warner Bros.'; 

// If you are ready to store the movie to the database (and rehydrate), run store()
$movie->store();

// This will now include an "id" 
return $movie; 

关于安全:使用预处理语句以及赋值保护的组合使处理用户输入变得安全(且方便)。

...
$movie = new MovieModel(Request::getInputs());
try{
    $movie->store();
} catch (\Exception $e) {
    // required field missing || validation failed || etc
}

模型类自动在现有条目和新条目之间切换模式。如果您遇到边缘情况,您可以手动更改模式。

...
// The following is NOT recommended in our scenario!
// This is only to show you the possibilities

$movie = new MovieModel();

// will return Neoan\Enums\TransactionType::INSERT
$mode = $move->getTransactionMode(); 
$movie->setTransactionType(TransactionType::UPDATE); 

检索和更新

如果您想修改现有记录,我们首先获取现有记录。

// sometimes I know the primary id ...
$matrix = MovieModel::get(1); 

// ... but often I lookup based on what I know
$matrix = MovieModel::retrieveOne([
            'name' => 'The Matrix'
          ]); 

// ... maybe I even want to create it if it doesn't exist
$matix = MovieModel::retrieveOneOrCreate([
            'name' => 'The Matrix'
          ]); 

// Let's fix the name
$matix->studio = 'Warner Bros. Pictures'

// Then simply store again
$matrix->store();

集合

集合是管理多个实例的同时的有用工具。每次检索多个记录时,都会返回一个集合。

集合是可迭代的,并具有以下附加功能

...
// First, lets retrieve multiple records
// Instead of "retrieveOne" we will use "retrieve"
// Additionally, we account for soft deleted records and 
// want to ignore them by adding a condition to our retrieval 
$allMovies = MovieModel::retrieve(['deletedAt' => null]);

// Collections are iterable
foreach($allMovies as $movie){
    ...
}

// However, it would be a shame if our modern IDE couldn't 
// help us with existing properties. So let's use "each" instead
$allMovies->each(function(MovieModel $movie, int $iteration){
    ...
});

// Did you do something to all the records there?
// Let's save all selected movies at once
$allMovies->store();

// While you can return collections directly, 
// you might need to convert them to an array
$flat = $allMovies->toArray();

// Didn't find what you are looking for?
// Just add to the existing collection
$allMovies->add(new MovieModel(['name' => 'Alien']))

分页

一个常见任务是分页集合,否则会太大。您可以方便地对每个模型进行分页。

...
$currentPage = 1;
$pageSize = 25;

return MovieModel::paginate($currentPage, $pageSize)

    // are there conditions/filters to this list?
    ->where(['studio' => 'Warner Bros. Pictures'])
    
    // controlling the sort
    ->descending('year')
    
    // finally, execute the pagination request
    ->get();

分页的响应是一个如下的数组

[
    'page' => 1,    // current page
    'total' => 50,      // total hits
    'pageSize' => 30,       // number of results per page
    'pages' => 2,       // total number of resulting pages
    'collection' => `{Collection}`      // result as Collection
]

迁移

您可能已经注意到没有文件处理迁移。相反,cli 会将现有表与您的模型定义进行比较,并相应地进行更新。然而,数据库上发生的事情不必对您不可见。基本命令 migrate:model $dialect $modelQualifiedName 有附加选项

  • with-copy (c)
  • output-folder (o)

示例

php cli migrate:model mysql App\Models\MovieModel -o migrations -c movie_backup

这将输出数据库操作到 sql 文件(在我们的案例中为 /src/migrations),并在执行任何更改命令之前创建一个名为 "movie_backup" 的表副本。

注意:输出文件夹必须位于 NeoanApp->appPath 之下

您还可以一次性迁移任何定义的自动加载命名空间中的所有模型。这在安装应用程序或合作时特别有用。

php cli migrate:models sqlite 

支持的方言

目前,以下方言直接支持

  • mysql
  • sqlite

然而,创建自己的命令很简单:查看 CLI

测试

核心本身使用 PHPUnit 和 CI,具有高测试覆盖率。未来我们希望提供应用程序测试的工具。现在,请自行实现自己的测试方法。

CLI

CLI 基于 symfony/console,并封装在一个容器中,这使得 neoan.io lenkrad 可供脚本使用。因此,您可以像平常一样将您自己的 symfony 命令添加到建议的文件 cli

#!/usr/bin/env php
<?php
...
$console = new Application($app);
$console->add(new MyOwnCommand($app));
...

查看可用命令

php cli list

贡献

目前我们只寻求反馈,因为市场规则和基础还没有固定下来。然而,请点赞、评论问题票据,帮助我们构建和改进这个轻量级解决方案。