ndufreche / tiimber
Requires
- mustache/mustache: ^2.14
- psr/log: ^3.0
- react/react: ^1.4
- symfony/routing: ^7.0
Requires (Dev)
- phpunit/phpunit: 11.0
This package is not auto-updated.
Last update: 2024-09-20 18:54:45 UTC
README
tiimber
本项目文件是为了创建一个非常小的博客,以便了解如何组织代码以及了解tiimber的工作原理。
需求
要使用Tiimber,您需要安装PHP 7和composer
安装Tiimber
创建composer.json
{ "require": { "ndufreche/tiimber": "dev-master" }, "autoload": { "psr-4": { "Blog\\": "Blog/" } } }
然后输入以下命令安装依赖项。
./composer.phar install
项目创建
在Tiimber中,您需要创建一个Application类,其中包含您的入口点,并且为了创建一个Tiimber App,您需要使用Tiimber ApplicationTrait。
创建Blog/Application.php
<?php namespace Blog; use Tiimber\Traits\{ApplicationTrait as Tiimber, ServerTrait as Server}; class Application { use Tiimber, Server; private function prepare() { $this->setRoot(dirname(__DIR__)); $this->setCacheFolder(dirname(__DIR__) . '/cache'); $this->setHost('localhost'); $this->setPort(1337); } public function start() { $this->prepare(); $this->chop(); $this->runHttpServer($this->runApp()); } }
然后我们需要创建一个index.php并调用您的Application。
创建index.php
<?php require __DIR__ . '/vendor/autoload.php'; (new Blog\Application())->start();
你好,世界
要创建一个“你好,世界”,我们需要三个组件:一个路由、一个布局和一个视图。
创建路由
创建config/routes.json
{ "hello": { "route": "/hello" } }
创建布局
一个最小的布局由一个常量组成。
常量TPL是您的模板。一个布局模板暴露出口和声明出口的方式是通过将它们封装在{{{ }}}
中。
在以下布局中,我们只暴露了一个出口content
。
创建Blog/Layouts/DefaultLayout.php
<?php namespace Blog\Layouts; use Tiimber\Layout; class DefaultLayout extends Layout { const TPL = '{{{content}}}'; }
创建视图
一个最小的视图由两个常量组成。
常量EVENTS是一个数组,其键代表要监听的事件和出口位置。
常量TPL是您的视图模板。
要创建HelloWorldView,我们需要将Hello world打印到在config/routes.json
文件中定义的hello
路由中接收到的request
的content
出口声明到DefaultLayout
中。
为此,创建视图如下。
创建Blog/Views/HelloWorldView.php
<?php namespace Blog\Views; use Tiimber\View; class HelloWorldView extends View { const EVENTS = [ 'request::hello' => 'content' ]; const TPL = 'Hello world.'; }
现在您可以测试它了
启动php服务器
php index.php
然后在您的浏览器中通过调用URL http://localhost:1337/hello 来尝试它。
模板布局
目前我们的布局非常简单,因此我们需要升级它以创建更多的出口。我们想要在布局中添加一个导航栏和页脚。
在Blog/Layouts/DefaultLayout.php中
<?php // ... class DefaultLayout extends Layout { const TPL = <<<HTML <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>My blog</title> </head> <body> <header> {{{navigation}}} </header> <div> {{{content}}} </div> <footer> {{{footer}}} </footer> </body> </html> HTML; }
现在我们已经创建了2个新的出口,然后我们需要渲染它。
创建导航视图
创建Blog/Views/NavigationView.php
<?php namespace Blog\Views; use Tiimber\View; class NavigationView extends View { const EVENTS = [ 'request::hello' => 'navigation' ]; const TPL = <<<HTML <ul> <li><a href="/"><h1>My Blog</h1></a></li> <li><a href="/hello">Hello</a></li> </ul> HTML; }
创建页脚视图
创建Blog/Views/FooterView.php
<?php namespace Blog\Views; use Tiimber\View; class FooterView extends View { const EVENTS = [ 'request::hello' => 'footer' ]; const TPL = 'Power by Tiimber'; }
现在您可以测试它了
重启php服务器
php index.php
如果您访问http://localhost:1337/hello,则没有问题,一切正常,但如果您点击我的博客
链接,则会显示一个空页面。
然后我们需要升级我们的NaviagtionView
和FooterView
以更全局地捕获请求事件。为此,我们需要使用通配符*
。
在Blog/Views/NavigationView.php中
<?php // ... class NavigationView extends View { const EVENTS = [ 'request::*' => 'navigation' ]; // ... }
在Blog/Views/FooterView.php中
<?php // ... class FooterView extends View { const EVENTS = [ 'request::*' => 'footer' ]; // ... }
现在重启php服务器
php index.php
然后,如果您访问http://localhost:1337/,您应该能看到您的导航栏和页脚。
渲染错误
目前我们没有定义主页,因此当我们访问http://localhost:1337/时,我们需要一个404未找到错误。
为此,我们需要创建一个新的视图。
创建Blog/Views/Errors/NotFoundView.php
<?php namespace Blog\Views\Errors; use Tiimber\View; class NotFoundView extends View { const EVENTS = [ 'error::404' => 'content' ]; const TPL = 'Error 404 page not found'; }
并且我们必须更新我们的导航栏和页脚
在Blog/Views/NavigationView.php中
<?php // ... class NavigationView extends View { const EVENTS = [ 'request::*' => 'navigation', 'error::*' => 'navigation' ]; // ... }
在Blog/Views/FooterView.php中
<?php // ... class FooterView extends View { const EVENTS = [ 'request::*' => 'footer', 'error::*' => 'footer' ]; // ... }
现在如果您重启服务器并访问主页,您一定会看到错误。
对于服务器错误,您可以这样做(但仅限于开发使用)
创建 Blog\Views\Errors\ServerErrorView.php
<?php namespace Blog\Views\Errors; use Tiimber\View; use Tiimber\Http\{Request, Response}; class ServerErrorView extends View { const EVENTS = [ 'error::500' => 'content' ]; const TPL = <<<EOF <div>{{message}}</div> <pre>{{stack}}</pre> EOF; private $error; public function onCall(Request $req, Response $res) { $this->error = $req->getArgs()->get('error'); } public function render(): array { return [ 'message' => $this->error->getMessage(), 'stack' => $this->error->getTraceAsString() ]; } }
使用记录器
您已经实现了一些视图和一些错误,但肯定想看到在Tiimber中发生了什么。为此,您需要使用记录器。目前Tiimber中有两种类型的记录器:FileLogger和SysLogger。在我们的例子中,我们将使用SysLogger。
在 Blog\Application.php
<?php // ... use Tiimber\Loggers\SysLogger as Logger; class Application { private function prepare() { // ... (new Logger()); } // ... }
就这些。
现在如果您重启服务器,当您从浏览器中调用页面时,您一定会看到很多行。
如果您想,您可以实现自己的记录器,为此,只需快速查看FileLogger或SysLogger。
动态化视图
您的Tiimber视图可以更加动态,这个教程的目标是创建一个简单的博客,然后我们需要创建和读取文章的能力。
Tiimber没有ORM,它让您有选择和实现的自由。
对于这个教程,我们使用一个名为RedBeanPHP的非常简单的ORM。
更新您的依赖项
在 composer.json
{ "require": { "ndufreche/tiimber": "dev-master", "gabordemooij/redbean": "dev-master" }, // ... }
然后安装它。
./composer.phar update
设置数据库
在 Blog\Application.php
<?php // ... use RedBeanPHP\R; class Application { // ... private function prepare() { R::setup('mysql:host=localhost;dbname=blog', 'user', 'password'); // ... } // ... }
现在创建一个主页
在 config/routes.json
{ "home": { "route": "/" }, // ... }
创建 Blog/Views/Articles/IndexView.php
<?php namespace Blog\Views\Articles; use Tiimber\View; use RedBeanPHP\R; class IndexView extends View { const EVENTS = [ 'request::home' => 'content' ]; const TPL = <<<HTML <ul> {{#articles}} <li> <a href="/article/{{id}}"> <h2>{{title}}</h2> <p> {{content}} </p> </a> </li> {{/articles}} </ul> {{^articles}} <p>No article available yet.</p> {{/articles}} HTML; public function render(): array { $articles = R::findAll('article','ORDER BY id DESC LIMIT 10'); return ['articles' => array_values($articles)]; } }
在 Article\IndexView中,我们使用一个名为render的新函数。这个方法只有一个目标,就是返回模板中需要的所有变量。
如果您重新启动服务器并访问主页,您会看到No article available yet.
。
创建一个文章页面
在 config/routes.json
{ // ... "article::show": { "route": "/{id}", "params": { "id": "[0-9]+" } }, // ... }
我们的文章路由需要是动态的,为此,在{}
之间传递动态参数,并在params部分添加正则表达式来验证参数。
创建 Blog/Views/Articles/ShowView.php
<?php namespace Blog\Views\Articles; use Tiimber\View; use Tiimber\Http\{Request, Response}; use RedBeanPHP\R; class ShowView extends View { const EVENTS = [ 'request::article::show' => 'content' ]; const TPL = <<<HTML <h2>{{article.title}}</h2> <p>{{article.content}}</p> HTML; private $article; public function onGet(Request $req, Response $res) { $this->article = R::load('article', (integer)$req->getArgs()->get('id')); } public function render(): array { return ['article' => $this->article]; } }
在这个视图中,我们使用了一个名为onGet
的新方法,它接收两个参数:请求和URL参数。args变量是一个数组,包含在路由article::show
中定义的id
。
有了这个id,我们可以加载我们的文章。
但我们的数据库中还没有文章,因此我们需要一种方法来提交它。
视图和操作
表单创建
这个部分的第一步是创建一个表单。
然后为此我们需要一个新路由和新视图。
在 config/routes.json
{ // ... "article::manage": { "route": "/article/{id}", "params": { "id": ".+" } }, // ... }
创建 Blog/Views/Articles/ManageView.php
<?php namespace Blog\Views\Articles; class ManageView extends ShowView { const EVENTS = [ 'request::article::manage' => 'content' ]; const TPL = <<<HTML <form method="post"> <input type="hidden" name="id" value="{{article.id}}"> <p><input type="text" name="title" placeholder="Title" value="{{article.title}}"></p> <p><textarea name="content">{{article.content}}</textarea></p> <p><button type="submit">Submit</button></p> </form> HTML; }
创建 Blog/Views/Articles/ShowView.php
<?php // ... class ShowView extends View { // ... const TPL = <<<HTML <h2>{{article.title}}</h2> <p>{{article.content}}</p> <p><a href="/article/{{article.id}}">edit</a></p> HTML; // ... }
在Blog/Views/NavigationView.php中
<?php // ... class NavigationView extends View { // ... const TPL = <<<HTML <ul> <li><a href="/"><h1>My Blog</h1></a></li> <li><a href="/hello">Hello</a></li> <li><a href="/article/new">New article</a></li> </ul> HTML; }
介绍动作
在Tiimber中,动作就像视图一样工作,不同之处在于它们没有任何要显示的内容。因此,它们不需要指定一个显示的位置或TPL常量或render方法。
在这种情况下,我们将使用动作来保存我们的文章。
创建 Blog/Actions/Articles/SaveAction.php
<?php namespace Blog\Actions\Articles; use Tiimber\Action; use Tiimber\Http\{Request, Response}; use RedBeanPHP\R; class SaveAction extends Action { use RedirectTrait; const EVENTS = [ 'request::article::manage' ]; private function prepare($id) { if ($id !== 'new') { return R::load('article', $id); } else { return R::dispense('article'); } } public function onPost(Request $req, Response $res) { $article = $this->prepare($req->getArgs()->get('id')); $article->title = $req->getPost->get('title'); $article->content = $req->getPost->get('content'); $id = R::store($this->article); $res->redirect('/'.$id); } }
在这个动作中,创建或更新文章,然后保存完成后重定向到文章。要重定向,您需要使用redirect trait,因为如果传统的headers()不够,您就不能停止服务器。
您可以看到我们使用了onPost
方法。通过这种方式,我们不会将onGet传递到视图中。指定method="post"
在您的表单中非常重要,以便访问视图和动作中的有效方法。
提示:视图和动作都可以访问onGet和onPost方法;
子渲染
为了完成我们的应用程序,我们将创建一个基于PHP会话的快速身份验证,并使用render事件添加新部分。
在Blog/Views/NavigationView.php中
<?php // ... use Tiimber\View; use Tiimber\Http\{Request, Response}; class NavigationView extends View { // ... const TPL = <<<HTML <ul> <li><a href="/"><h1>My Blog</h1></a></li> <li><a href="/hello">Hello</a></li> {{#user}} <li><a href="/article/new">New article</a></li> {{/user}} <li>{{{login}}}</li> </ul> HTML; $this->logged= false; public function onCall(Request $req, Response $res) { $this->logged = $req->getSession()->has('user'); } public function render(): array { return [ 'user' => $this->logged ]; } }
在导航中,我们创建了一个名为login
的输出,并检查会话中是否有用户以添加新文章链接。
然后我们创建一个将渲染到login
输出中的视图。
创建 LoginView
创建 Blog\Views\User\LoginView.php
<?php namespace Blog\Views\User; use Tiimber\View; use Tiimber\Http\{Request, Response}; class LoginView extends View { const EVENTS = [ 'render::navigation' => 'login' ]; const TPL = <<<HTML {{#user}} <b>Hello {{user}}!</b> {{/user}} {{^user}} <form method="post" action="/login"> <input type="text" name="username" placeholder="Username"> <button type="submit">Submit</button> </form> {{/user}} HTML; private $user; public function onCall(Request $res, Response $res) { $this->user = $res->getSession()->get('user'); } public function render(): array { return [ 'user' => $this->user ]; } }
这个视图监听render::navigation
事件。
创建 LoginAction
就像文章中提到的那样,我们将创建一个操作来在会话中保存我们的用户。
在 config/routes.json
{ // ... "user::auth": { "route": "/login" }, // ... }
创建 Blog/Actions/Users/AuthAction.php
<?php namespace Blog\Actions\Users; use Tiimber\Action; use Tiimber\Http\{Request, Response}; class AuthAction extends Action { use RedirectTrait; const EVENTS = [ 'request::user::auth' ]; public function onPost(Request $req, Response $res) { $req->getSession()->set('user', $req->getPost()->get('username')); $res->redirect('/'); } }
然后我们将升级之前的视图和操作
在 Blog/Actions/Articles/SaveAction.php 中
<?php // ... use Tiimber\Action; use Tiimber\Http\{Request, Response}; // ... class SaveAction extends Action { // ... public function onPost(Request $req, Response $res) { // ... $article->author = $req->getSession()->get('user'); $id = R::store($this->article); $res->redirect('/'.$id); } }
创建 Blog/Views/Articles/ShowView.php
<?php namespace Blog\Views\Articles; use Tiimber\View; use Tiimber\Http\{Request, Response}; use RedBeanPHP\R; class ShowView extends View { // ... const TPL = <<<HTML <h2>{{article.title}}</h2> <p>{{article.content}}</p> {{#user}} <p><a href="/article/{{article.id}}">edit</a></p> {{/user}} {{#article.author}} <p>Created by {{article.author}}</p> {{/article.author}} HTML; private $article; private $logged; public function onGet(Request $req, Response $res) { $this->article = R::load('article', (integer)$req->getArgs()->get('id')); $this->logged = $req->getSession()->get('user'); } public function render(): array { return [ 'article' => $this->article, 'user' => $this->logged ]; } }
现在您可以享受您的小 tiimber 应用程序了。