建立完整的MVC架構

到目前為止,我們都只用上一個 controller,也就是 BlogController,我們稱這個 controller 為 base contrtoller。

Base Controller 是專門用來處理View顯示邏輯的,我們應該要能夠有更多的 controller 來處理不同 MVC 的儲存邏輯,例如 Article controller 專門儲存 article 的儲存。因此,現在我們先來拆分 controller 吧。

建立 article controller

我們先建立一個 BlogControllerArticlecontrollers 目錄下面,並實作一個 foo() task:

<?php
// administrator\components\com_blog\controllers\article.php

use Joomla\CMS\MVC\Controller\BaseController;

defined('_JEXEC') or die;

class BlogControllerArticle extends BaseController
{
    public function foo()
    {
        echo 'Article controller foo';
    }
}
 

現在到瀏覽器輸入 index.php?option=com_blog&task=article.foo,會看到此畫面:

p-2014-09-03-4.jpg

這意味著,原本我們用 task=xxx 會指向到 BlogController::xxx() ,而只要改用 task=article.xxx 就會前往 BlogControllerArticle::xxx(),應該很好理解吧。 那麼讓我們先把 save()add() 移過來:

<?php
// administrator\components\com_blog\controllers\article.php

use Joomla\CMS\MVC\Controller\BaseController;

defined('_JEXEC') or die;

class BlogControllerArticle extends BaseController
{
    public function add()
    {
        $this->setRedirect(JRoute::_('index.php?option=com_blog&view=article&layout=edit', false));
    }

    public function save()
    {
        // 只取得 POST 的資料
        $post = $this->input->post;

        // 將 POST 資料塞進一個陣列中,用 getString() 避免不合法字元
        $data['title']     = $post->getString('title');
        $data['alias']     = $post->getString('alias');
        $data['created']   = $post->getString('created');

        // HTML 資料必須用 getRaw(),不然會被過濾掉
        $data['introtext'] = $post->getRaw('introtext');
        $data['fulltext']  = $post->getRaw('fulltext');

        // 取得 Article Model 並執行 save()
        $model = $this->getModel('Article');

        $model->save($data);

        // save() 完成後我們跳回 Article List 頁面
        $this->setRedirect(JRoute::_('index.php?option=com_blog&view=articles', false));
    }
}
 

清空 BlogController:

<?php
// administrator\components\com_blog\controller.php

use Joomla\CMS\MVC\Controller\BaseController;

defined('_JEXEC') or die;

class BlogController extends BaseController
{
    protected $default_view = 'articles';
}
 

我們連 display() 都移除掉,為什麼呢?因為 BaseController 已經幫我們寫好預設的 display() 了,其實我們只要設定好 default_view,剩下的都交給 parent 執行即可。一開始寫給大家看的只是範例而已。

別忘了現在 task 必須前綴 article. ,所以更改一下 View 裡面的 ToolbarHelper 吧:

<?php
// administrator\components\com_blog\views\article\view.html.php

// ...

public function addToolbar()
{
    ToolbarHelper::title('Article Edit', 'pencil');

    ToolbarHelper::save('article.save');
}
 
<?php
// administrator\components\com_blog\views\articles\view.html.php

// ...

public function addToolbar()
{
    ToolbarHelper::title('Articles');

    ToolbarHelper::addNew('article.add');
}
 

好了,現在再跑一次所有頁面並按下儲存吧,所有功能都不會改變,儲存功能應該也要正常運作,我們只是做了重構。

加入取消功能

我們的編輯頁面還沒有取消功能,現在把它加上去吧。先打開 BlogViewArticle 並加入 cancel 的 toolbar button:

<?php
// administrator\components\com_blog\views\article\view.html.php

// ...

public function addToolbar()
{
    ToolbarHelper::title('Article Edit', 'pencil');

    ToolbarHelper::save('article.save');
    ToolbarHelper::cancel('article.cancel');
}
 

再到 article controller 中加入 cancel() task,redirect 回到 articles list 頁面:

<?php
// administrator\components\com_blog\controllers\article.php

use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route;

defined('_JEXEC') or die;

class BlogControllerArticle extends BaseController
{
    // ...

    public function cancel()
    {
        $this->setRedirect(JRoute::_('index.php?option=com_blog&view=articles', false));
    }
}
 

完成就可以測試囉,編輯頁面應該會出現「取消」按鈕,按下後就會回到 articles list 頁面:

p-2014-09-03-5.jpg

加入刪除功能

聰明的人應該也知道刪除要怎麼做了,一樣先加入 delete() task 在 article controller 內:

<?php
// administrator\components\com_blog\controllers\article.php

use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route;

defined('_JEXEC') or die;

class BlogControllerArticle extends BaseController
{
    public function delete()
    {
        $id = $this->input->get('id');

        if (!$id)
        {
            $this->setRedirect(JRoute::_('index.php?option=com_blog&view=articles', false), '沒有 ID', 'warning');

            return false;
        }

        $model = $this->getModel('Article');

        $model->delete($id);

        $this->setRedirect(JRoute::_('index.php?option=com_blog&view=articles', false), '刪除成功');
    }
}
 

這回我們替 redirect 加上訊息(Flash message),並從網址中拿出 id 值交給 model 刪除。而 model 自然也需要一個 delete() method,這次的 delete() 我們就寫的簡潔一點吧:

<?php
// administrator\components\com_blog\models\article.php

use Joomla\CMS\MVC\Model\BaseDatabaseModel;

defined('_JEXEC') or die;

class BlogModelArticle extends BaseDatabaseModel
{
    // ...

    public function delete($id)
    {
        $sql = "DELETE FROM #__blog_articles WHERE id = " . (int) $id;

        return $this->_db->setQuery($sql)->execute();
    }
}
 

下一步,我們到 article list 的 default.php template 裡面,替每一個 record 加上刪除按鈕:

<?php
// administrator\components\com_blog\views\articles\tmpl\default.php

use Joomla\String\StringHelper;

defined('_JEXEC') or die;
?>
<form action="<?php echo JUri::getInstance(); ?>" id="adminForm" name="adminForm" method="post">
    <table class="table table-striped">
        <thead>
        <tr>
            <th>ID</th>
            <th>Title</th>
            <th>Intro</th>
            <th>Delete</th>
        </tr>
        </thead>
        <tbody>
        <?php foreach ($this->items as $item): ?>
            <tr>
                <td><?php echo $item->id; ?></td>
                <td><?php echo $this->escape($item->title); ?></td>
                <td><?php echo StringHelper::substr(strip_tags($item->introtext), 0, 50); ?></td>
                <td>
                    <a href='<?php echo JRoute::_('index.php?option=com_blog&task=article.delete&id=' . $item->id) ?>' class="btn">
                        <span class="icon-trash text-error"></span>
                    </a>
                </td>
            </tr>
        <?php endforeach; ?>
        </tbody>
    </table>

    <div class="hidden-inputs">
        <input type="hidden" name="option" value="com_blog" />
        <input type="hidden" name="task" value="" />
    </div>
</form>

 

每一顆刪除按鈕都會導向到 index.php?option=com_blog&task=article.delete&id={id},這樣子就可以針對這個 id 來刪除,然後再導向回來。我們來試試看吧,先按下刪除按鈕:

p-2014-09-03-6.jpg

如果成功的話就會出現刪除成功訊息。

p-2014-09-03-7.jpg

假設我們手動丟出沒有 id 的網址,則會出現錯誤訊息:

p-2014-09-03-8.jpg

目前 Joomla 提供四種 flash message 類型:

p-2014-09-03-9.jpg

編輯舊文章功能

顯示原有的資料

我們只寫了新增功能,但是別忘了還要有編輯功能,先在 articles list template 加上編輯連結,task=article.edit

<?php
// administrator\components\com_blog\views\articles\tmpl\default.php
?>
<!-- 更改 title -->

    <td>
        <a href='<?php echo JRoute::_("index.php?option=com_blog&task=article.edit&id=" . $item->id); ?>'>
            <?php echo $this->escape($item->title); ?>
        </a>
    </td>

 

然後在 article controller 加上 edit() task:

<?php
// administrator\components\com_blog\controllers\article.php

use Joomla\CMS\MVC\Controller\BaseController;
use Joomla\CMS\Router\Route;

defined('_JEXEC') or die;

class BlogControllerArticle extends BaseController
{
    public function edit()
    {
        $id = $this->input->get('id');

        $this->setRedirect(JRoute::_('index.php?option=com_blog&view=article&layout=edit&id=' . $id, false));
    }

    // ...
}
 

依舊只做重新導向,有人會問說,幹嘛不直接連結到編輯的 view 就好了?還要花那麼多力氣做重新導向?這方面未來會再說明,總之我們先乖乖的做重導向吧,這寫法是在為未來更強大的功能鋪路。

準備好 Article Model getItem(),這裡我們直接在 model 用 input 取得 id,Joomla 的 Model 比較像其他框架的 Repository,可以呼叫 input 另外還有更嚴謹但較麻煩的寫法,未來有機會再說明

<?php
// administrator\components\com_blog\models\article.php

use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\CMS\Factory;

defined('_JEXEC') or die;

class BlogModelArticle extends BaseDatabaseModel
{
    public function getItem()
    {
        $input = Factory::getApplication()->input;

        $id = $input->get('id');

        if (!$id)
        {
            return false;
        }

        $sql = "SELECT * FROM #__blog_articles WHERE id = " . $id;

        return $this->_db->setQuery($sql)->loadObject();
    }

    // ...
}
 

然後在 Article View 裡面呼叫 Model 的 getItem(),還是一樣用之前提到的簡潔寫法就能呼叫 model 內的 method 了。

這次我們把回傳的 item 不管有沒有值,都包進 JData 物件中,為什麼要這樣呢?因為 JData 會避免我們跟物件取值時,因為index不存在而造成錯誤訊息。舉例說明,假設 model 回傳 false (找不到資料),我們執行 echo $this->item->title 就會報錯,因為 false 不是物件,或者該物件沒有 title 這個屬性。但我們把 $this->item 先用 JData 包起來後,在執行一次 echo $this->item->title 則不會報錯,只會印出空值,因為 JData 的 getter 能夠自動處理不存在的值。

好了,別忘紀要把 introtext 與 fulltext 塞進 editor 內:

<?php
// administrator\components\com_blog\views\article\view.html.php

use Joomla\CMS\Editor\Editor;
use Joomla\CMS\Factory;
use Joomla\CMS\MVC\View\HtmlView;
use Joomla\CMS\Toolbar\ToolbarHelper;

defined('_JEXEC') or die;

class BlogViewArticle extends HtmlView
{
    public function display($tpl = null)
    {
        // 跟 Model 要資料
        $this->item = $this->get('Item');

        // 包裝進 JData 方便取資料
        $this->item = new JData($this->item);

        // 取得全站設定中的編輯器設定檔
        $config = Factory::getConfig();

        // 呼叫編輯器物件,直接 render 出來
        $this->introEditor = Editor::getInstance($config->get('editor'))->display('introtext', $this->item->introtext, '600px', '300px', 50, 15);
        $this->fullEditor = Editor::getInstance($config->get('editor'))->display('fulltext', $this->item->fulltext, '600px', '300px', 50, 15);

        $this->addToolbar();

        parent::display($tpl);
    }

    // ...
}
 

打開 template ,我們要替每一個 input 塞入 $this->item 內的值,這次要注意,在最下面的 hidden inputs 那邊我們加入一個 id 欄位:

<?php
// administrator\components\com_blog\views\article\tmpl\edit.php

defined('_JEXEC') or die;
?>
<form action="<?php echo JUri::getInstance(); ?>" id="adminForm" name="adminForm" method="post">
    <fieldset class="form-horizontal">
        <legend>Blog Info</legend>

        <!-- Title -->
        <div class="control-group">
            <label for="form-title" class="control-label">Title</label>
            <div class="controls">
                <input type="text" id="form-title" name="title" value="<?php echo $this->item->title; ?>" />
            </div>
        </div>

        <!-- Alias -->
        <div class="control-group">
            <label for="form-alias" class="control-label">Alias</label>
            <div class="controls">
                <input type="text" id="form-alias" name="alias" value="<?php echo $this->item->alias; ?>" />
            </div>
        </div>

        <!-- Created -->
        <div class="control-group">
            <label for="form-created" class="control-label">Created Time</label>
            <div class="controls">
                <?php echo JHtml::calendar($this->item->created, 'created', 'form-created'); ?>
            </div>
        </div>
    </fieldset>

    <fieldset>
        <legend>Text</legend>

        <!-- Intro text -->
        <div class="control-group row-fluid">
            <label for="form-title" class="control-label">Intro Text</label>
            <div class="controls span6">
                <?php echo $this->introEditor; ?>
            </div>
        </div>

        <hr />

        <!-- Full text -->
        <div class="control-group row-fluid">
            <label for="form-alias" class="control-label">Full text</label>
            <div class="controls span6">
                <?php echo $this->fullEditor; ?>
            </div>
        </div>
    </fieldset>

    <div class="hidden-inputs">
        <input type="hidden" name="id" value="<?php echo $this->item->id; ?>" />
        <input type="hidden" name="option" value="com_blog" />
        <input type="hidden" name="task" value="" />
    </div>
</form>
 

現在,回到 article list 點擊某一個 record,應該可以看到編輯頁面秀出這個 record 的內容:

p-2014-09-03-12.jpg

p-2014-09-03-11.jpg

更新舊有資料

假設我們做了簡單的編輯,現在要把原有的 data 儲存回去。

首先,我們必須取得舊的 id,這個數字剛剛已經被放在 hidden input 裡面了,所以按下儲存時應該會一起送出去,那我們就到 controller 內把它拿出來吧:

<?php
// administrator\components\com_blog\controllers\article.php

defined('_JEXEC') or die;

class BlogControllerArticle extends JControllerLegacy
{
    // ...

    public function save()
    {
        // 只取得 POST 的資料
        $post = $this->input->post;

        // 將 POST 資料塞進一個陣列中,用 getString() 避免不合法字元
        $data['id']      = $post->getInt('id');
        $data['title']   = $post->getString('title');
        $data['alias']   = $post->getString('alias');
        $data['created'] = $post->getString('created');

        // ...
    }

    // ...
}
 

controller 的 save 內只要加一行即可,比較麻煩的在 model 的 save,我們要這樣改寫:

<?php
// administrator\components\com_blog\models\article.php

defined('_JEXEC') or die;

class BlogModelArticle extends JModelLegacy
{
    // ...

    public function save($data)
    {
        $db = $this->_db;

        $id = $data['id'];

        // 把 $data 內的每個元素用單引號包起來,並且跳脫不合法字元
        foreach ($data as &$value)
        {
            $value = $db->quote($value);
        }

        // 有 id 時用 update
        if ($id)
        {
            $sql = "UPDATE #__blog_articles SET "
                . "title = " . $data['title']
                . ", alias = " . $data['alias']
                . ", created = " . $data['created']
                . ", introtext = " . $data['introtext']
                . ", `fulltext` = " . $data['fulltext']
                . " WHERE id = " . $data['id'];
        }

        // 沒有 id 時用 insert
        else
        {
            $sql = "INSERT INTO #__blog_articles"
                . " (title, alias, created, introtext, `fulltext`) "
                . " VALUES (" . implode(', ', $data) . ")";
        }

        $db->setQuery($sql);

        return $db->execute();
    }

    // ...
}

 

好了,這樣子應該要能夠正常儲存了。原理其實也很簡單,沒有 id 的時候用 insert 插入新資料,有 id 的時候則改用 update 並且 where id = x 來更新舊資料。如果你把 sql 印出來,應該要長這樣子:

UPDATE #__blog_articles SET title = 'Test2', alias = 'test2', created = '2014-09-17', introtext = '<p>活潑,扮一個漁翁,我敢說,</p>\r\n<p>你去時也還是一個光亮</p>', `fulltext` = '<p>只要你自己心靈上不長瘡瘢,也把你的影像</p>\r\n<p>卻偏不作聲,我猜想,你回到了天父的懷抱</p>' WHERE id = '6'
 

接下來,我們實際按下儲存看看,應該會看到剛剛做的修改被儲存成功了:

p-2014-09-03-13.jpg

小結

到此為止,我們初階 MVC 架構算是完成了,該有的 CRUD(Create, Read, Update, Delete) 皆已備齊,元件也有個基本的雛形,如果要開發簡單應用的話,在這樣的架構下已經可以運作了。

p-2014-09-03-10.jpg

之後的教學,我們將著重在如何強化這些功能,提供更完善的 UI 介面與進階操作,以及尚未觸及的更多 Joomla Advanced MVC 等等強大的 RAD 功能。


本章節的教學內容可以在 GitHub 上找到: Code / Commits

ukash birim çevirme saç ekimi estetik