Update Validation Rules for Articles: src/Model/Table/ArticlesTable.php
class ArticlesTable extends Table
{
public function initialize(array $config): void
{
$this->addBehavior('Timestamp');
}
public function beforeSave(EventInterface $event, $entity, $options)
{
if ($entity->isNew() && !$entity->slug) {
$sluggedTitle = Text::slug($entity->title);
// trim slug to maximum length defined in schema
$entity->slug = substr($sluggedTitle, 0, 191);
}
}
public function validationDefault(Validator $validator): Validator
{
$validator
->notEmptyString('title')
->minLength('title', 10)
->maxLength('title', 255)
->notEmptyString('body')
->minLength('body', 10);
return $validator;
}
}
Add Delete Action
L src/Controller/ArticlesController.php
public function delete($slug){
$this->request->allowMethod(['post', 'delete']);
$article = $this->Articles->findBySlug($slug)->firstOrFail();
if($this->Articles->delete($article)){
$this->Flash->success(__('The {0} article has been deleted.', $article->title));
return $this->redirect(['action'=> 'index']);
}
}
templates/Articles/index.php
<td>
<?= $this->Html->link('Edit', ['action' => 'edit', $article->slug]) ?>
<?= $this->Form->postLink(
'Delete',
['action' => 'delete', $article->slug],
['confirm' => 'Are you sure?'])
?>
</td>
### Tags and User
$ bin/cake bake model users
$ bin/cake bake controller users
$ bin/cake bake template users
$ bin/cake bake all tags
Updating Articles to enable Tagging
// in src/Controller/ArticlesController.php
public function add()
{
$article = $this->Articles->newEmptyEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->getData());
// Hardcoding the user_id is temporary, and will be removed later
// when we build authentication out.
$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')->all();
$this->set('tags', $tags);
$this->set('article', $article);
}
templates/Articles/add.php:
echo $this->Form->control('tags._ids', ['options' => $tags]);
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')->all();
$this->set('tags', $tags);
$this->set('article', $article);
}
Finding Articles by Tags
// config/routes.php
$builder->scope('/articles', function(RouteBuilder $builder){
$builder->connect('/tagged/*', ['controller' => 'Articles', 'action'=>'tags']);
});
src/Controller/ArticlesController.php
public function tags(){
$tags = $this->request->getParam('pass');
$articles = $this->Articles->find('tagged', [
'tags' => $tag
])
->all();
$this->set([
'articles' => $articles,
'tags' => $tags
]):
}
Creating the Finder Method
// src/Model/Table/ArticlesTable.php
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'])){
$qeury->leftJoinWith('Tags')
->where(['Tags.title IS' => null]);
} else {
$query->innerJoinWith('Tags')
->where(['Tags.title IN' => $options['tags']]);
}
return $query->group(['Articles.id']);
}
Creating the View
// templates/Articles/tags.php
<h1>
Articles tagged with
<?= $this->Text->toList(h($tags), 'or') ?>
</h1>
<section>
<?php foreach($articles as $article): ?>
<h4><?= $this->Html->link(
$article->title,
['controller' => 'Articles', 'action' => 'view', $article->slug]
) ?></h4>
<span><?= h($article->created) ?></span>
<?php endforeach; ?>
</section>
Improving the Tagging Experience
– Adding a computed field
// src/Model/Entity/Article.php
protected function _getTagString(){
if(isset($this->_fields['tag_string'])){
return $this->_fields['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, ', ')
}
Updating the views
// templates/Articles/add.php
echo $this->Form->control('tag_string', ['type' => 'text']);
// templates/Articles/view.php
<h1><?= h($article->title) ?></h1>
<p><?= h($article->body) ?></p>
<p><b>Tags:</b><?= h($article->tag_string) ?></p>
public function view($slug)
{
$article = $this->Articles->findBySlug($slug)->contain('Tags')->firstOrFail();
$this->set(compact('article'));
}
Persisting the Tag String
// src/Model/Table/ArticlesTable.php
public function beforeSave(EventInterface $event, $entity, $options)
{
if($entity->tag_string) {
$entity->tags = $this->_buildTags($entity->tag_string);
}
if ($entity->isNew() && !$entity->slug) {
$sluggedTitle = Text::slug($entity->title);
// trim slug to maximum length defined in schema
$entity->slug = substr($sluggedTitle, 0, 191);
}
}
protected function _buildTags($tagString){
$newTags = array_map('trim', explode(',', $tagString));
$newTags = array_filter($newTags);
$newTags = array_unique($newTags);
$out = [];
$tags = $this->Tags->find()
->where(['Tags.title IN' => $newTags])
->all();
foreach ($tags->extract('title') as $existing){
$index = array_search($existing, $newTags);
if($index !== false){
unset($newTags[$index]);
}
}
foreach($tags as $tag){
$out[] = $tag;
}
foreach($newTags as $tag){
$out[] = $this->Tags->newEntity(['title'=>$tag]);
}
return $out;
}
Auto-populating the Tag String
// src/Model/Table/ArticlesTable.php
public function initialize(array $config): void
{
$this->addBehavior('Timestamp');
$this->belongsToMany('Tags', [
'joinTable' => 'articles_tags',
'dependent' => true
]);
}