在路由中从数据库映射 slug

2021-12-21 00:00:00 php cakephp-3.0 cakephp

我必须支持项目的 url 友好结构.

有多个带有 slug 列的表,在 cakephp 中如何以最有效的方式将 slug 路由到控制器.

起初我正在检查表中是否存在 slug,如果存在 slug 使用路由:

 $c = TableRegistry::get('cateogories');$result= $c->find()->select(['id'])->where(['url'=>$slug])->toArray();如果(计数($ 结果)> 0){$routes->connect('/:slug',['控制器' =>'类别', '动作' =>'索引', 'id' =>$result[0]['id']]);}

问题是我有多个检查,如上面的一个,即使路由先前匹配(不需要运行,因此调用额外的查询),每个检查都在运行.

那么我如何添加某种条件语句,以便它只检查路由是否匹配,如果之前的路由都不匹配.

解决方案

我建议使用处理此问题的自定义路由类.虽然您可以查询路由文件中的数据,但这是

  • 不要过度测试友好
  • 不太干
  • 反向路由不安全

后一点意味着当没有连接所有路由时,尝试从路由数组中为未连接的路由生成 URL 可能会触发异常,或者匹配错误的路由.

使用自定义路由类,您可以在连接路由时简单地在选项中传递模型,并在 解析 URL,查询给定 slug 的模型,并返回 false 或相应地解析数据.真的很简单,看看现有的路由类是做什么的.

这是一个非常基本的例子,应该是不言自明的.

src/Routing/Route/SlugRoute.php

命名空间 AppRoutingRoute;使用 CakeRoutingRouteRoute;使用 CakeORMLocatorLocatorAwareTrait;类 SlugRoute 扩展 Route{使用 LocatorAwareTrait;公共函数解析($url){$params = parent::parse($url);如果 (!$params ||!isset($this->options['model'])){返回假;}$count = $this->tableLocator()->get($this->options['model'])-> 查找()->哪里(['slug' =>$params['slug']])-> 计数();如果 ($count !== 1) {返回假;}返回 $params;}}

此示例假设在控制器中,您将使用 slug 来检索记录.如果您希望传递 ID,则可以不使用 count(),而是获取 ID 并将其传递到已解析的数据中,例如:

$params['pass'][] = $id;

它最终会作为控制器动作的第二个参数被传递.

routes.php

$routes->connect('/:slug',['控制器' =>'文章', '动作' =>'看法'],['通过' =>['蛞蝓'],'routeClass' =>'SlugRoute','模型' =>'文章']);$routes->connect('/:slug',['控制器' =>'类别', '动作' =>'看法'],['通过' =>['蛞蝓'],'routeClass' =>'SlugRoute','模型' =>'类别']);//...

这将首先检查 Articles 模型,然后是 Categories 模型等,并在其中一条路线找到给定 slug 的记录后停止.><小时>

另见

  • 食谱 > 路由 > 自定义路由课程
  • API > CakeRoutingRoute::parse()
  • 来源> CakeRoutingRoute

I have to support url friendly structure for a project.

There is multiple tables with a slug column, in cakephp how can I route the slug to a controller in the most efficient way.

At first I was checking if slug exist in a table, if slug exist use the route:

    $c = TableRegistry::get('cateogories');
    $result= $c->find()->select(['id'])->where(['url'=>$slug])->toArray();
    if(count($result) > 0) {
        $routes->connect(
            '/:slug',
            ['controller' => 'Categories', 'action' => 'index', 'id' => $result[0]['id']]
        );
    }

The problem being that I have multiple checks like the one above and each one is being ran even if a route prior matches (doesn't need to be ran so extra querys are being called).

So how can I add a conditional statement of some sort so that it only checks if the route matches if none of the prior ones have.

解决方案

I'd suggest to go for a custom route class that handles this. While you could query the data in your routes files, this is

  • not overly test friendly
  • not very DRY
  • not safe for reverse routing

The latter point means that when not connecting all routes, trying to generate a URL from a route array for a non-connected route might trigger an exception, or match the wrong route.

With a custom route class you could simply pass the model in the options when connecting the routes, and in the route class after parsing the URL, query that model for the given slug, and return false or the parsed data accordingly.It's really simple, just have a look at what the existing route classes do.

Here's a very basic example which should be pretty self-explantory.

src/Routing/Route/SlugRoute.php

namespace AppRoutingRoute;

use CakeRoutingRouteRoute;
use CakeORMLocatorLocatorAwareTrait;

class SlugRoute extends Route
{
    use LocatorAwareTrait;

    public function parse($url)
    {
        $params = parent::parse($url);
        if (!$params ||
            !isset($this->options['model'])
        ) {
            return false;
        }

        $count = $this
            ->tableLocator()
            ->get($this->options['model'])
            ->find()
            ->where([
                'slug' => $params['slug']
            ])
            ->count();

        if ($count !== 1) {
            return false;
        }

        return $params;
    }
}

This example assumes that in the controller, you'd use the slug to retrieve the record. If you'd wanted to have the ID passed, then instead of using count(), you could fetch the ID and pass it along in the parsed data, like:

$params['pass'][] = $id;

It would then end up being passed as the second argument of the controller action.

routes.php

$routes->connect(
    '/:slug',
    ['controller' => 'Articles', 'action' => 'view'],
    [
        'pass' => ['slug'],
        'routeClass' => 'SlugRoute',
        'model' => 'Articles'
    ]
);

$routes->connect(
    '/:slug',
    ['controller' => 'Categories', 'action' => 'view'],
    [
        'pass' => ['slug'],
        'routeClass' => 'SlugRoute',
        'model' => 'Categories'
    ]
);

// ...

This would first check the Articles model, then the Categories model, etc., and stop once one of the routes finds a record for the given slug.


See also

  • Cookbook > Routing > Custom Route Classes
  • API > CakeRoutingRoute::parse()
  • Source > CakeRoutingRoute

相关文章