ndufreche/tiimber

框架

dev-master 2024-02-05 12:42 UTC

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路由中接收到的requestcontent出口声明到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,则没有问题,一切正常,但如果您点击我的博客链接,则会显示一个空页面。

然后我们需要升级我们的NaviagtionViewFooterView以更全局地捕获请求事件。为此,我们需要使用通配符*

在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 应用程序了。