2024年8月6日 星期二

PHP 開發筆記 - 嘗試使用 Laravel + Twill CMS Toolkit 開發 CMS 後台管理服務 @ macOS M1



近期公司內部服務都朝向 Laravel framework 來維護,其中常見需求是製作具有 CMS 管理的機制,提供不同部門的同事編輯資料及發佈出去。就來試試看 Twill 這個 CMS Toolkit 套件。從他的文件得知,引入他可以快速擁有後台登入機制,以及省去自己規劃資料庫的資料表,而相較 wordpress 則是有更大的彈性做事。

目前在 Macbook M1 和 MacPorts 環境下,操作一下,先弄個 Laravel project 出來:

```
% sudo port install php83 php83-iconv php83-intl php83-mbstring php83-openssl php83-curl php83-sqlite php83-zip php83-gd php83-exif
% alias php=php83
% wget https://getcomposer.org/download/latest-stable/composer.phar -O /tmp/composer.phar
% php /tmp/composer.phar self-update
% alias composer="php /tmp/composer.phar"
% composer create-project --prefer-dist laravel/laravel /tmp/laravel-workspace
Creating a "laravel/laravel" project at "/tmp/laravel-workspace"
Installing laravel/laravel (v11.1.4)
  - Installing laravel/laravel (v11.1.4): Extracting archive
Created project in /tmp/laravel-workspace
> @php -r "file_exists('.env') || copy('.env.example', '.env');"
Loading composer repositories with package information
Updating dependencies
...
```
這邊偷懶用 alias composer="php /tmp/composer.phar" ,後面會碰到類似的的錯誤訊息時,其實就是找不到 composer 指令:

```
   Symfony\Component\Process\Exception\ProcessStartFailedException 

  The command "'composer' 'dump-autoload'" failed.

Working directory: /private/tmp/laravel-workspace

Error: proc_open(): posix_spawn() failed: No such file or directory
```

因此可以把 /tmp/composer.phar 擺到 PATH 內會尋找指令的地方,或是人工再補個 composer dump-autoload 等等

```
% cp /tmp/composer.phar ~/.bin/composer
% chmod 755 ~/.bin/composer
```

接下來安裝 Twill Toolkit,整個流程其實參考 Twill 官網教學文即可:Building a simple page builder with Laravel Blade,在此僅記錄一下操作流程:

```
% cd /tmp/laravel-workspace
laravel-workspace % composer require area17/twill:"^3.0"
laravel-workspace % php artisan twill:install
...
Let's create a superadmin account!

 Enter an email:
 > user@example.com

 Enter a password:
 > 

 Confirm the password:
 > 

Your account has been created
All good!
```

接著安裝後台模組 Pages:

```
laravel-workspace % php artisan twill:make:module pages

 Do you need to use the block editor on this module? [yes]:
  [0] no
  [1] yes
 > 

 Do you need to translate content on this module? [yes]:
  [0] no
  [1] yes
 > 

 Do you need to generate slugs on this module? [yes]:
  [0] no
  [1] yes
 > 

 Do you need to attach images on this module? [yes]:
  [0] no
  [1] yes
 > 

 Do you need to attach files on this module? [yes]:
  [0] no
  [1] yes
 > 

 Do you need to manage the position of records on this module? [yes]:
  [0] no
  [1] yes
 > 

 Do you need to enable revisions on this module? [yes]:
  [0] no
  [1] yes
 > 

 Do you need to enable nesting on this module? [no]:
  [0] no
  [1] yes
 > 

 Do you also want to generate a model factory? [yes]:
  [0] no
  [1] yes
 > 

 Do you also want to generate a model seeder? [yes]:
  [0] no
  [1] yes
 > 

Migration created successfully! Add some fields!

   INFO  Factory [database/factories/PageFactory.php] created successfully.  

Models created successfully! Fill your fillables!
Repository created successfully! Control all the things!
Controller created successfully! Define your index/browser/form endpoints options!
Form request created successfully! Add some validation rules!

 Do you also want to generate the preview file? [yes]:
  [0] no
  [1] yes
 > 

   INFO  Seeder [database/seeders/PageSeeder.php] created successfully.  

The following snippet has been added to routes/twill.php:
-----
TwillRoutes::module('pages');
-----
To add a navigation entry add the following to your AppServiceProvider BOOT method.
-----
use A17\Twill\Facades\TwillNavigation;
use A17\Twill\View\Components\Navigation\NavigationLink;

public function boot()
{
    ...
    
    TwillNavigation::addLink(
        NavigationLink::make()->forModule('pages')
    );
}
-----
Do not forget to migrate your database after modifying the migrations.

Enjoy.
```

上述的意思是 routes/twill.php 已經增加好後台管理介面的 routing 規則:

```
% cat routes/twill.php 
<?php

use A17\Twill\Facades\TwillRoutes;

// Register Twill routes here eg.
// TwillRoutes::module('posts');

TwillRoutes::module('pages');
```

但是 app/Providers/AppServiceProvider.php 必須自己處理,調整成下方:

```
laravel-workspace % cat app/Providers/AppServiceProvider.php 
<?php

namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use A17\Twill\Facades\TwillNavigation;
use A17\Twill\View\Components\Navigation\NavigationLink;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     */
    public function register(): void
    {
        //
    }

    /**
     * Bootstrap any application services.
     */
    public function boot(): void
    {
        TwillNavigation::addLink(
            NavigationLink::make()->forModule('pages')
        );
    }
}
```

這些更動是讓後台 /admin 時,上方導覽可以多一項 Pages 的功能,後續透過下方來使用:

laravel-workspace % php artisan migrate
laravel-workspace % php artisan serve --host 0.0.0.0 --port 8000

如此,就可以用 http://localhost:8000/admin 登入後台,點擊 Pages 切換到 http://localhost:8000/admin/pages 可以新增 Pages ,可點擊 Add new 按鈕一則,這時可以看到前台網址規則是 localhost/en/pages/hello-world ,但實際上在 PHP Laravel routing 規則中,前台網址規則都還沒實作處理,所以是看不到資料的。


上述僅做了簡易的後台搭建,後續要處理的有:
  1. 讓後來編輯界面更加豐富,例如有更多的元件(image, text)等,主要是擴增編輯使用的表單元素
  2. 處理後台編輯時,preview 缺少的 css 資源 (運行 npm install && npm run build)
  3. 建立新元素的樣板資料
  4. 建立前台網頁的網址規則跟處理的 Controller (PageDisplayController)
如此,在後台把頁面發布出去後,就可以在前台被瀏覽到。但光上述四點的操作項目是不少的。

接著來進行,也就是 Twill 官網導覽流程,並且把其他碰到的問題也解一解:Configuring the page module

由於預設的前台網頁網址規則是跟語言相關的,例如建立個 Hello World Page 後,可以看到他的前台網址是 localhost/en/pages/hello-world ,若要去掉語言,就是調整 app/Http/Controllers/Twill/PageController.php :

```
 18     protected function setUpController(): void
 19     {
 20         $this->setPermalinkBase('');          // 去掉 /pages/ 那層網址
 21         $this->withoutLanguageInPermalink();  // 去掉 /en/ 那層網址 
 22     }
```

接著來調整撰文時的表單功能,例如目前每一個 Page 都可以填寫 title 跟 description 可增加 SEO 的效果,而增加文章分享後的美觀,則是要增加圖片,這時直接修改 app/Http/Controllers/Twill/PageController.php ,添加發文時可以上傳圖片功能:

```
  6 use A17\Twill\Services\Forms\Fields\Medias;
...
 29     public function getForm(TwillModelContract $model): Form
 30     {   
 31         $form = parent::getForm($model);
 32         
 33         $form->add(
 34             Input::make()->name('description')->label('Description')->translatable()
 35         );
 36         
 37         $form->add(
 38             Medias::make()->name('cover')->label('Cover image')
 39         );
 40         
 41         return $form;
 42     }
...
```

主要是新增引入 `use A17\Twill\Services\Forms\Fields\Medias;` 跟 `$form->add( Medias::make()->name('cover')->label('Cover image') );`


這時我們的 laravel 是透過 artisan 跑在 8000 port,拖拉上傳圖片時,其實會顯示不出來


因為他的圖片網址規則並沒有帶 port ,且只要把網址複製出來加上 port number 就可以正常顯示,代表只需處理 Laravel framework 的 .env 中 APP_URL 規則:

```
% cat .env | grep APP_URL
APP_URL=http://localhost
```

這時建議測試時可以有兩套環境設置,運行時指定設定檔案:

```
% cp .env .env.local
% cat .env.local | grep APP_URL 
APP_URL=http://localhost:8000
% php artisan serve --host 0.0.0.0 --port 8000 --env local

   INFO  Server running on [http://0.0.0.0:8000].  

  Press Ctrl+C to stop the server
```

如此也解掉後台圖片顯示失敗的問題。


下一刻則是擴充文章編輯環境,引入 Block Editor 架構,也就是在編輯文章時,可以拖拉區塊到文章內,還可以自行開發,把常用的項目元件化。從 Twill 官網的範例資訊,預設有兩個 Block 了,一個是 image block ,另一個是 wysiwyg block,官網範例會試著新增一個小的區塊,例如名為 Text 的區塊。

首先,先啟用 Block Editor,啟用方式是在 app/Http/Controllers/Twill/PageController.php 裡引入 `use A17\Twill\Services\Forms\Fields\BlockEditor;` 和增加 `$form->add( BlockEditor::make() );`

接著則是試著建立一個 text block:

```
laravel-workspace % php artisan twill:make:block text

 Should we also generate a view file for rendering the block? (yes/no) [no]:
 > yes

Creating block...
File: /private/tmp/laravel-workspace/resources/views/twill/blocks/text.blade.php
Block text was created.
Block text blank render view was created.
Block is ready to use with the name 'text'

laravel-workspace % cat resources/views/twill/blocks/text.blade.php
@twillBlockTitle('Text')
@twillBlockIcon('text')
@twillBlockGroup('app')

<x-twill::input
    name="title"
    label="Title"
    :translated="true"
/>

<x-twill::wysiwyg
    name="text"
    label="Text"
    placeholder="Text"
    :toolbar-options="[
        'bold',
        'italic',
        ['list' => 'bullet'],
        ['list' => 'ordered'],
        [ 'script' => 'super' ],
        [ 'script' => 'sub' ],
        'link',
        'clean'
    ]"
    :translated="true"
/>
```

可以看到其樣板也長出來了


但是在後台 preview 時,仍會有待處理的訊息:

This is a basic preview. You can use dd($block) to view the data you have access to. <br />This preview file is located at: /private/tmp/laravel-workspace/resources/views/site/blocks/text.blade.php

將他修改一下:

```
% cat resources/views/site/blocks/text.blade.php
<div class="prose">
    <h2>{{$block->translatedInput('title')}}</h2>
    {!! $block->translatedInput('text') !!}
</div>
```    


接著再產生另一個 image block 並更新他的 block view 跟 preview:

```
laravel-workspace % php artisan twill:make:block image

 Should we also generate a view file for rendering the block? (yes/no) [no]:
 > yes

Creating block...
File: /private/tmp/laravel-workspace/resources/views/twill/blocks/image.blade.php
Block image was created.
Block image blank render view was created.
Block is ready to use with the name 'image'

laravel-workspace % cat resources/views/twill/blocks/image.blade.php
@twillBlockTitle('Image')
@twillBlockIcon('text')
@twillBlockGroup('app')
 
<x-twill::medias
    name="highlight"
    label="Highlight"
/>

laravel-workspace % cat resources/views/site/blocks/image.blade.php 
<div class="py-8 mx-auto max-w-2xl flex items-center">
    <img src="{{$block->image('highlight', 'desktop')}}"/>
</div>
```

此外,image block 要生效還需要調整 config/twill.php

```
% cat config/twill.php 
<?php

return [
    'block_editor' => [
        'crops' => [ 
            'highlight' => [
                'desktop' => [
                    [
                        'name' => 'desktop',
                        'ratio' => 16 / 9,
                    ],
                ],
                'mobile' => [
                    [
                        'name' => 'mobile',
                        'ratio' => 1,
                    ],
                ],
            ],
        ],
    ],
];
```

接著,還要幫前後台產的文章添加 CSS 效果 `@vite('resources/css/app.css')` ,添加方式是修改 `resources/views/site/layouts/block.blade.php` 跟 `resources/views/site/page.blade.php`

```
laravel-workspace % cat resources/views/site/layouts/block.blade.php 
<!doctype html>
<html lang="en">
<head>
    <title>#madewithtwill website</title>
    @vite('resources/css/app.css') 
</head>
<body>
<div>
    @yield('content')
</div>
</body>
</html>

laravel-workspace % cat resources/views/site/page.blade.php
<!doctype html>
<html lang="en">
<head>
    <title>{{ $item->title }}</title>
    @vite('resources/css/app.css') 
</head>
<body>
<div class="mx-auto max-w-2xl">
    {!! $item->renderBlocks() !!}
</div>
</body>
</html>
```

此外,還要用 npm 工具編譯出 resources/css/app.css:

```
% nvm use v20
Now using node v20.9.0 (npm v10.3.0)

% npm install        

added 23 packages, and audited 24 packages in 577ms

5 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

laravel-workspace % tree resources/css/       
resources/css/
└── app.css

1 directory, 1 file
```

如此,在 Twill 後台編輯文章時,就可以在 Block editor 操作下增加文字區塊、圖片等等的功能。

不過,直到現在前台機制還沒打通,尚未提供前台 routing rule 等顯示前台網頁,先建立個 PageDisplayController 來處理:

```
% php artisan make:controller PageDisplayController

   INFO  Controller [app/Http/Controllers/PageDisplayController.php] created successfully.  
```

把 PageDisplayController 更新為可以接一個參數,並且立刻把它印出 debug 訊息:

```
% cat app/Http/Controllers/PageDisplayController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Contracts\View\View;

class PageDisplayController extends Controller
{
    public function show(string $slug): View
    {
        dd($slug);
    }
}
```

接著,再把 routing 設置好:

```
% cat routes/web.php 
<?php

use Illuminate\Support\Facades\Route;

//Route::get('/', function () {
//    return view('welcome');
//});

Route::get('{slug}', [\App\Http\Controllers\PageDisplayController::class, 'show'])->name('frontend.page'); 
```

如此在前台瀏覽網頁就會看到:

最後,再把 PageDisplayController 調整成顯示正確的資料:

```
laravel-workspace % cat app/Http/Controllers/PageDisplayController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Contracts\View\View;
use App\Repositories\PageRepository;

class PageDisplayController extends Controller
{
    //public function show(string $slug): View
    //{
    //    dd($slug);
    //}

    public function show(string $slug, PageRepository $pageRepository): View
    {
        $page = $pageRepository->forSlug($slug);
 
        if (!$page) {
            abort(404);
        }
 
        return view('site.page', ['item' => $page]);
    }
}
```

如此瀏覽前台時,例如 http://localhost:8000/hello-world 就可以顯示網頁內容了。

最後,如果碰到前台圖片沒有顯示出來的部分,則是留意拖拉建立圖片時,需要依照不同裝置版型做設定,例如 PC 瀏覽時看不到圖片,那應當是少設定的 desktop 的設置:

```
laravel-workspace % cat resources/views/site/blocks/image.blade.php
<div class="py-8 mx-auto max-w-2xl flex items-center">
    <img src="{{$block->image('highlight', 'desktop')}}"/>
</div>
```

需留意在 block editor 時,其 Image 拖拉進去時,有沒有 `desktop crop` 的描述

沒有留言:

張貼留言