Cakephp 3 – Hướng dẫn tạo Tags

Ở bài trước chúng ta đã làm một CMS nhỏ cho bài viết(Articles), bài này chúng ta sẽ làm thêm Tags, trong database đã có sẳn bảng tags rồi. Cũng tương tự như bài viết(Articles) đều có các chức năng cơ bản: thêm, sửa, xoá, update.

Các bạn có thể sử dụng Base Console để tạo toàn bộ các file cần thiết bằng cú pháp:

bin/cake bake all tags

Nhưng ở đây mình không hướng dẫn phần đó, chỉ làm theo cách cơ bản nhất là copy file đang có để tạo ra file cần thiết.

Các bước thực hiện:

Tạo file src/Model/Entity/Tag.php có nôi dung như sau:

<?php
namespace App\Model\Entity;
use Cake\ORM\Entity;
class Tag extends Entity
{
protected $_accessible = [
'title' => true,
'created' => true,
'modified' => true,
'articles' => true
];
}

Tạo file src/Model/Table/TagsTable.php nội dung:


<?php
namespace App\Model\Table;
use Cake\ORM\Query;
use Cake\ORM\Table;
class TagsTable extends Table
{
public function initialize(array $config)
{
parent::initialize($config);

$this->setTable('tags');
$this->setDisplayField('title');
$this->setPrimaryKey('id');

$this->addBehavior('Timestamp');

$this->belongsToMany('Articles', [
'foreignKey' => 'tag_id',
'targetForeignKey' => 'article_id',
'joinTable' => 'articles_tags'
]);
}
}

Tạo file src/Controller/TagsController.php nội dung như bên dưới:

<?php
namespace App\Controller;
use App\Controller\AppController;
class TagsController extends AppController
{
public function index()
{
$tags = $this->paginate($this->Tags);

$this->set(compact('tags'));
}
public function view($id = null)
{
$tag = $this->Tags->get($id, [
'contain' => ['Articles']
]);
$this->set('tag', $tag);
}
public function add()
{
$tag = $this->Tags->newEntity();
if ($this->request->is('post')) {
$tag = $this->Tags->patchEntity($tag, $this->request->getData());
if ($this->Tags->save($tag)) {
$this->Flash->success(__('The tag has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The tag could not be saved. Please, try again.'));
}
$articles = $this->Tags->Articles->find('list', ['limit' => 200]);
$this->set(compact('tag', 'articles'));
}
public function edit($id = null)
{
$tag = $this->Tags->get($id, [
'contain' => ['Articles']
]);
if ($this->request->is(['patch', 'post', 'put'])) {
$tag = $this->Tags->patchEntity($tag, $this->request->getData());
if ($this->Tags->save($tag)) {
$this->Flash->success(__('The tag has been saved.'));

return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('The tag could not be saved. Please, try again.'));
}
$articles = $this->Tags->Articles->find('list', ['limit' => 200]);
$this->set(compact('tag', 'articles'));
}
public function delete($id = null)
{
$this->request->allowMethod(['post', 'delete']);
$tag = $this->Tags->get($id);
if ($this->Tags->delete($tag)) {
$this->Flash->success(__('The tag has been deleted.'));
} else {
$this->Flash->error(__('The tag could not be deleted. Please, try again.'));
}
return $this->redirect(['action' => 'index']);
}
}

Tiếp theo là phần Template cho view của Tag gồm: add.ctp, edit.ctp, index.ctp và view.ctp. Nội dung các file này các bạn lấy trong phần Download nhá.

Sau các phần trên thì các bạn hãy thêm dữ liệu cho bảng tags và chọn luôn các Bài viết. Và nói thêm là mình sẽ cập nhật lại giao diện cho các view của bài viết(Articles) để nhìn nó đẹp và thống nhất với bên Tags.

Phần Tags trên khi bạn thêm sẽ có thể lựa chọn luôn bài viết(articles), phân tiếp theo chúng ta sẽ cập nhật lại phần bài viết(articles) có thể lựa chọn thẻ (tags)

Thêm Tags vào Bài viết – Articles

Một bài viết có thể có nhiều tags và một tags cũng có nhiều bài viết được nên giữa Bài viết và Tags có mối quan hệ nhiều nhiều. Chúng ta sẽ thêm đoạn sau vào trong phương thức initialize của src/Model/Table/ArticlesTable.php

$this->belongsToMany('Tags');

Về mối quan hệ giữa các model chúng ta sẽ có một bài riêng.

Cập nhật lại 2 action add và edit trong src/Controller/ArticlesController.php, chúng ta sẽ lấy dữ liệu tags đang có để hiển thị ngoài view.

Action add() sẽ như sau:

public function add()
{
$article = $this->Articles->newEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->getData());
$article->user_id = 1;

if ($this->Articles->save($article)) {
$this->Flash->success(__('Your article has been saved.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Unable to add your article.'));
}
$tags = $this->Articles->Tags->find('list');

$this->set('tags', $tags);
$this->set('article', $article);
}

tương tự cho action edit():

public function edit($slug)
{
$article = $this->Articles->findBySlug($slug)->contain('Tags')->firstOrFail();
if ($this->request->is(['post', 'put'])) {
$this->Articles->patchEntity($article, $this->request->getData());
if ($this->Articles->save($article)) {
$this->Flash->success(__('Your article has been updated.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Unable to update your article.'));
}
$tags = $this->Articles->Tags->find('list');

$this->set('tags', $tags);
$this->set('article', $article);
}

Thêm đoạn sau vào 2 template hiển thị src/Template/Articles/add.ctp, src/Template/Articles/edit.ctp:

echo $this->Form->control('tags._ids', ['options' => $tags]);

Tiến hành thêm bài viết, và chọn các tags tương thích, kết quả là đã thêm tags thành công, truy cập lại danh sách bài viết(articles) và danh sách thẻ(tags) để xem kết quả.

Tìm kiếm bài viết bằng thẻ tag thì như thế nào?

Trong quá trình thực hiện giả sử các bạn cần chạy url như sau: http://localhost:8888/cakephp_3_7_2/articles/tagged/ho nghĩa là tìm tất cả các bài viết có thẻ tag = ‘ho‘ chúng ta sẽ tiến hành như sau:

Đầu tiên ta thay đổi một tý trong config/routes.php, thêm đoạn sau vào:

Router::scope(
'/articles',
['controller' => 'Articles'],
function ($routes) {
$routes->connect('/tagged/*', ['action' => 'tags']);
}
);

Đoạn router trên tương ứng với controller: Articles, acction: tags(), và tương ứng với url: /articles/tagged/ , nếu bây giờ truy cập http://localhost:8888/cakephp_3_7_2/articles/tagged thì sẽ có lỗi xuất hiện do chúng ta chưa thêm action tags() trong controller src/Controller/ArticlesController.php. Tiến hành thêm vào:

public function tags()
{
// The 'pass' key is provided by CakePHP and contains all
// the passed URL path segments in the request.
$tags = $this->request->getParam('pass');
// Use the ArticlesTable to find tagged articles.
$articles = $this->Articles->find('tagged', [
'tags' => $tags
]);
$this->set([
'articles' => $articles,
'tags' => $tags
]);
}

Đoạn code phía trên có $this->request->getParam(‘pass’), giá trị ‘pass‘ được Cake dùng để lấy toàn bộ những gì phía sau url được định nghĩa cụ thể đây là (/articles/tagged/), ví dụ bạn nhập url: http://localhost:8888/cakephp_3_7_2/articles/tagged/abc/def thì khi sử dụng  $this->request->getParam(‘pass’nó sẽ có giá trị là array ([0]=>abc [1]=>def) 

Tiếp theo là đoạn $articles = $this->Articles->find(‘tagged’, [‘tags’ => $tags]); đồng nghĩa với việc nó sẽ gọi một hàm mà chúng ta định nghĩa trong src/Model/Table/ArticlesTable.php có tên là findTagged(), 2 chổ màu đen các bạn chú ý nhá, phải giống nhau nó mới chính xác được. Nội dung như sau:

// add this use statement right below the namespace declaration to import
// the Query class
use Cake\ORM\Query;
// The $query argument is a query builder instance.
// The $options array will contain the 'tags' option we passed
// to find('tagged') in our controller action.
public function findTagged(Query $query, array $options)
{
$columns = [
'Articles.id', 'Articles.user_id', 'Articles.title',
'Articles.body', 'Articles.published', 'Articles.created',
'Articles.slug',
];
$query = $query
->select($columns)
->distinct($columns);
if (empty($options['tags'])) {
// If there are no tags provided, find articles that have no tags.
$query->leftJoinWith('Tags')
->where(['Tags.title IS' => null]);
} else {
// Find articles that have one or more of the provided tags.
$query->innerJoinWith('Tags')
->where(['Tags.title IN' => $options['tags']]);
}
return $query->group(['Articles.id']);
}

Trong đoạn trên là các cách thức thực hiện JOIN, DISTINCT trong CakePHP, và phương pháp của hàm findTagged trên chúng ta có thể tận dụng nó cho việc sử dụng lại khi nào cần. Đây là một hỗ trợ trong CakePHP.

Tạo template cho tags() action: tiến hành tạo file src/Template/Articles/tags.ctp

<h1>
Articles tagged with
<?= $this->Text->toList(h($tags), 'or') ?>
</h1>
<section>
<?php foreach ($articles as $article): ?>
<article>
<!-- Use the HtmlHelper to create a link -->
<h4><?= $this->Html->link(
$article->title,
['controller' => 'Articles', 'action' => 'view', $article->slug]
) ?></h4>
<span><?= h($article->created) ?></span>
</article>
<?php endforeach; ?>
</section>

Trong đoạn trên có sử dụng các HtmlHelper và Text để tạo html, còn có h() mã hoá đầu ra trong html để tránh bị HTML injection.

Bây giờ các bạn có thể nhập http://localhost:8888/cakephp_3_7_2/articles/tagged/ho nó sẽ xuất hiện các bài viết có thẻ tag = ho, và nếu nhập là http://localhost:8888/cakephp_3_7_2/articles/tagged/ho/post thì xuất hiện các bài viết có thể ho hoặc post nhá.

Cải thiện thêm Tags trong Bài viết

Với cách thức thêm tag trên, thì chúng ta cần tạo các tag cần thiết rồi khi thực hiện thêm bài viết sẽ chọn tag, làm như thế có hơi rườm rà, chúng ta tiến hành nâng cao hơn thêm các tags trực tiếp trong khi thêm bài viết luôn. Cách thực hiện như sau:

Thêm một trường(field) – tag_string ảo vào trong Article, chỉ là một trường tạm để thông qua Article mà chúng ta sẽ lấy được tag, việc này được thực hiện trong src/Model/Entity/Article.php 

// the Collection class
use Cake\Collection\Collection;
protected function _getTagString()
{
if (isset($this->_properties['tag_string'])) {
return $this->_properties['tag_string'];
}
if (empty($this->tags)) {
return '';
}
$tags = new Collection($this->tags);
$str = $tags->reduce(function ($string, $tag) {
return $string . $tag->title . ', ';
}, '');
return trim($str, ', ');
}

Từ đây khi cần chúng ta có thể sử dụng $article->tag_string, để truy xuất giá trị của tag_string

Cập nhật view cho bài viết

Thay thế đoạn echo $this->Form->control(‘tags._ids’, [‘options’ => $tags]); bằng echo $this->Form->control(‘tag_string’, [‘type’ => ‘text’]); Tiến hành cập nhật lại đoạn đó ở cả 2 file src/Template/Articles/add.ctp and src/Template/Articles/edit.ctp

Thay thế trên nghĩa là thay vì thêm bài viết chỉ việc chọn các tag cần thiết, thì ở đây sẽ thành nhập các tag mà bạn muốn và nếu như nhiều tags thì các tag được phân cách nhau bằng dấu ‘,‘  trong input text. Cuối cùng để lưu vào table tags thì cần thực hiện trong hàm beforeSave() của src/Model/Table/ArticlesTable.php:


public function beforeSave($event, $entity, $options)
{
if ($entity->tag_string) {
$entity->tags = $this->_buildTags($entity->tag_string);
}

}

protected function _buildTags($tagString)
{
// Trim tags
$newTags = array_map('trim', explode(',', $tagString));
// Remove all empty tags
$newTags = array_filter($newTags);
// Reduce duplicated tags
$newTags = array_unique($newTags);

$out = [];
$query = $this->Tags->find()
->where(['Tags.title IN' => $newTags]);

// Remove existing tags from the list of new tags.
foreach ($query->extract('title') as $existing) {
$index = array_search($existing, $newTags);
if ($index !== false) {
unset($newTags[$index]);
}
}
// Add existing tags.
foreach ($query as $tag) {
$out[] = $tag;
}
// Add new tags.
foreach ($newTags as $tag) {
$out[] = $this->Tags->newEntity(['title' => $tag]);
}
return $out;
}

Đến đây nếu thực hiện thêm hoặc chỉnh sửa bài viết và nhập luôn phần tag vào trong input, nó sẽ được tự động lưu vào table tags, rất tiện lợi phải không nào.

Lưu ý: trong action edit() của src/Controller/ArticlesController.php có đoạn $this->Articles->findBySlug($slug)->contain(‘Tags’)->firstOrFail(); phần in đậm màu đó là để lấy ra các dữ liệu liên quan đến tags cụ thể ở đây là title được viết trong hàm _getTagString() nếu không có contain(‘Tags’), thì phần tags của bài viết không hiển thị ra được. Các bạn tự làm trong action view() nhá.

Đến đây mình xin kết thúc phần hướng dẫn tạo thẻ tags cho bài viết. Ở bài tiếp theo sẽ là phần User: login, logout…

Kết luận

  1. Nếu có thắc mắc gì các bạn để lại comment bên dưới mình sẽ trả lời sớm nhất có thể.
  2. Cảm ơn các bạn đã đọc.

Nongdanit.info
[Cakephp 3] Bài 3 – Hướng dẫn tạo Tags