afterflow/recipe

Laravel 生成器库

0.1.0 2020-02-23 10:41 UTC

This package is auto-updated.

Last update: 2024-09-29 05:22:23 UTC


README

Recipe 是一个基于 Laravel 组件构建的生成器框架,允许您根据提供的数据和模板生成任何内容。

本库的主要目标是提供创建生成器所需的工具。

要求

尽管它依赖于 Laravel 框架的 Blade 模板系统,但此库不需要 Laravel,它只拉取其中的一些组件。

这意味着您可以安全地将它包含在自己的框架无关的 composer 包中。

安装

composer require afterflow/recipe 0.1.*

基本用法

假设我们有一个简单的 Blade 模板

{{-- templates/user.blade.php --}}

Name: {{$name}}
Last Name: {{ $last_name }}

让我们使用 Recipe 编译一个字符串

$recipe = new \Afterflow\Recipe\Recipe();
$data   = $recipe->with([ 'name' => 'Vlad', 'last_name' => 'Libre' ])
                 ->template(__DIR__ . '/templates/user.blade.php')
                 ->render();

返回结果


Name: Vlad
Last Name: Libre

所以基本上我们只是编译了一个带有给定数据的 Blade 模板。但那并不是重点。现在让我们看看一些高级用法。

自定义 Recipe 类

让我们构建一个简单的自定义 recipe,我们可以重用它或将其嵌套在其他 recipe 中。它将从模板创建一个类并返回源代码。

模板

{{--templates/class.blade.php--}}

{{-- Otherwise this file will be treated as PHP script--}}
{!! '<'.'?php' !!}

@unless(empty( $namespace ))
namespace {{ $namespace }};
@endunless

@unless(empty( $imports ))
    @foreach( $imports as $import)
import {{ $import }};
    @endforeach
@endunless

class {{ $name }} {{ isset($extends) ? 'extends '. $extends : '' }} {{ !empty($implements) ? 'implements '. collect($implements)->implode(', ') : '' }}
{
@unless(empty($traits))
    use {{ collect($traits)->implode(', ') }};
@endunless

@isset($content)
{{--This function indents each line of $content string with 4 spaces--}}
@indent($content,4)
@endisset
}

Recipe

<?php

namespace Afterflow\Recipe\Recipes;

use Afterflow\Recipe\Recipe;

class ClassRecipe extends Recipe
{

    protected $template = __DIR__ . '/../../templates/class.blade.php';

    protected $props = [
        'name'       => [
            'rules' => 'required|string',
        ],
        'extends'    => [
            'default' => '',
            'rules' => 'string',
        ],
        'namespace'  => [
            'rules' => 'string',
        ],
        'content'    => [
            'default' => '',
            'rules'   => 'string',
        ],
        'imports'    => [
            'default' => [],
            'rules'   => 'array',
        ],
        'implements' => [
            'default' => [],
            'rules'   => 'array',
        ],
        'traits'     => [
            'default' => [],
            'rules'   => 'array',
        ],
    ];

}

用法

$data = ( new ClassRecipe() )->with([
    'namespace' => 'App',
    'name'      => 'User',
    'extends'   => 'Authenticatable',

    'imports' => [
        'Illuminate\Foundation\Auth\User as Authenticatable',
        'Illuminate\Notifications\Notifiable',
        'Laravel\Passport\HasApiTokens',
    ],

    'traits'     => [
        'HasApiTokens',
        'Notifiable',
    ],
    'implements' => [ 'SomeInterface', 'OtherInterface' ],
])->render();

由于我们现在使用自己扩展了 RecipeClassRecipe 类,所以这里发生的变化不多。这允许我们在类内部定义模板,并使用更短的用法语法。

在这里,您会注意到我们定义了一个新的 $props 变量,它与 VueJs 组件中使用的变量有些相似。

首先,我们添加了一些验证,告诉 Recipe 在此 recipe 中 name 数据属性是必需的。您可以像在 Laravel 应用程序中通常那样定义验证规则 - 这是一回事。

其次,我们为导入设置了默认值。如果用户没有提供任何输入,则将应用这些默认值。

不渲染构建数据

有时可能只需要从用户输入和定义的属性构建数据,而不使用任何模板

$data = ( new ClassRecipe() )->with([
    'namespace' => 'App',
    'name'      => 'User',
    'extends'   => 'Authenticatable',

    'imports' => [
        'Illuminate\Foundation\Auth\User as Authenticatable',
        'Illuminate\Notifications\Notifiable',
        'Laravel\Passport\HasApiTokens',
    ],

    'traits'     => [
        'HasApiTokens',
        'Notifiable',
    ],
    'implements' => [ 'SomeInterface', 'OtherInterface' ],
])->build();

如果您调用 build() 而不是 render(),您将获得在原始输入上应用属性后归一化的数据。您可以使用这些数据在其他 recipe 中创建复合生成器。

替代语法

在构建复杂的嵌套 recipe 时,简短的语法可能很有用。

// Full syntax
$recipe = (new ClassRecipe())->with($data)->render();

// Pass data into constructor:
$recipe = (new ClassRecipe($data))->render();

// Less braces:
$recipe = ClassRecipe::make($data);

// If your recipe defines a template or a custom render() function:
$string = ClassRecipe::quickRender($data);

// Compile data only:
$data = ClassRecipe::quickBuild($data);

在渲染之前准备模板数据

有时在将数据发送到 Blade 编译器之前转换数据很有用。如果您的 recipe 有 dataForTemplate() 方法,则其返回值将用作模板的数据。

<?php

namespace Afterflow\Recipe\Recipes;

use Afterflow\Recipe\Recipe;

class FunctionRecipe extends Recipe
{

    protected $template = __DIR__ . '/../../templates/function.blade.php';

    protected $props = [
        'name'       => [
            'rules' => 'required',
        ],
        'arguments'  => [],
        // ...
    ];

    public function dataForTemplate()
    {

        $data = $this->data();

        $data['arguments'] = collect($data['arguments'])->implode(', ');

        return $data;
    }
}

自定义渲染

通过在 recipe 中重写 render() 方法,您可以创建没有模板的 recipe 或定义任何其他自定义渲染逻辑。只需确保在内部调用 $this->build() 以应用属性。

<?php

namespace Afterflow\Recipe\Recipes;

use Afterflow\Recipe\Recipe;

class ClassVarRecipe extends Recipe
{

    protected $props = [
        'name'       => [
            'rules' => 'required|string',
        ],
        'visibility' => [
            'rules' => 'string|in:public,private,protected',
        ],
        'value'      => [
            'default' => '',
            'rules' => 'string',
        ],
        'static'     => [
            'default' => false,
            'rules'   => 'boolean',
        ],
        'const'      => [
            'default' => false,
            'rules'   => 'boolean',
        ],
        'docBlock'   => [
            'default' => '',
            'rules'   => 'string',
        ],
    ];

    public function render()
    {

        $string = '';

        if ($v = $this->data('docBlock')) {
            $string .= $v . PHP_EOL;
        }

        if ($v = $this->data('visibility')) {
            $string .= $v . ' ';
        }

        if ($this->data('static')) {
            $string .= 'static ';
        }

        if ($this->data('const')) {
            $string .= 'const ';
        }

        $string .= $this->data('name');
        if ($v = $this->data('value')) {
            $string .= ' = ' . $v;
        }

        $string .= ';';

        return $string;
    }
}

实现流畅式 API

为了让过程更加有趣和简单,您可以在 recipe 上撒一些流畅性

    // ...


    public function name($value)
    {
        return $this->input('name', $value);
    }

    public function value($value)
    {
        return $this->input('value', $value);
    }

    public function const()
    {
        return $this->input('const', true);
    }

    // ...

然后您可以使用此 recipe 如此

$data = ClassVarRecipe::make()->name( '$name' )
                      ->protected()
                      ->value( '"Vlad"' )
                      ->docBlock( '// First Name' )
                      ->render();

或在使用 recipe 时使用 MagicSetters trait 以实现相同的功能(这不会处理 IDE 突出显示);

嵌套 recipe

现在让我们看看这有多么强大

        /**
         * This recipe nests other recipes and shows alternative syntax to pass data through constructor
         */
        $data = ClassRecipe::make()->namespace('App')->name('User')->content(
        /**
         * See ClassVarRecipe to learn how to render things without template
         */
            Recipe::sequence([
                ClassVarRecipe::make()->protected()->name('$name')->docBlock('// First Name')->render(),
                ClassVarRecipe::make()->protected()->name('$lastName')->docBlock('// Last Name')->render(),
                /**
                 * See ClassVarRecipe to learn how to filter data before render
                 */
                ConstructorRecipe::make()->arguments([
                    'string $name',
                    'string $lastName',
                ])->body('$this->name = $name;' . eol() . '$this->lastName = $lastName;')->render(),
                FunctionRecipe::make()->name('getLastName')->return('$this->lastName;')->render(),
                FunctionRecipe::make()->name('getName')->return('$this->name;')->render(),
            ], eol(2))
        )->render();

由于 Recipe 实现了 __toString,您可以省略这里的 render() 调用。

这将产生

<?php

namespace App;


class User  
{

    // First Name
    protected $name = "Vlad";
    
    // Last Name
    protected $lastName;
    
    function __construct(string $name, string $lastName)
    {
        $this->name = $name;
        $this->lastName = $lastName;
    }
    
    function getLastName()
    {
        return $this->lastName;
    }
    
    function getName()
    {
        return $this->name;
    }
    
}

现在您可以通过扩展或嵌套 ClassRecipe 来创建 Laravel 模型 recipe,并创建一个控制台命令来生成模型或执行任何其他疯狂的事情。

您可以使用的内置 recipe(工作正在进行中)

  • ClassRecipe
  • ClassVarRecipe
  • FunctionRecipe
  • MethodCallRecipe
  • ConstructorRecipe
  • Laravel/Models/ModelRecipe
  • laravel/Models/Relations/RelationRecipe

指令

@indent($string, $spaces = 4) - indents each line of string output
@sequence($array, $glue = ', ') - useful to render arrays

辅助函数

所有辅助函数都有命名空间

function q( $what ) {
	return "'" . $what . "'";
}

function qq( $what ) {
	return '"' . $what . '"';
}

function eol( $times = 1 ) {
	$str = '';
	for ( $i = 0; $i < $times; $i ++ ) {
		$str .= PHP_EOL;
	}

	return $str;
}

function arr( $what, $d = [ '[', ']' ] ) {
	return Recipe::array( $what, $d );
}