优雅地自定义遍历输出文章

楚天乐 1145 0 条

背景介绍

看了不下十个主题的代码,一个通病,就是控制代码和模板混写在一起。如下图
QQ图片20180821114548.png

这么写的问题是什么?
当输出结构比较复杂的时候,html会变得非常难以修改。

typecho自己是如何做的

作为一个typecho新手,很好奇为什么系统内置的文章输出就可以如此优雅。一个while($this->next())实现文章遍历,模板随意修改,数据控制代码也随意修改,互不干涉。

以下是所有模板中都用到的,输出文章列表的代码。

<?php while($this->next()): ?>
    <article class="post_summary">
        <h3 class="title"><a target="_blank" itemtype="url" href="<?php $this->permalink() ?>"><?php $this->title() ?></a></h3>
        <div class="row">
            <div class="col-lg-3">
                <a href="<?php $this->permalink() ?>" class="post_thumbnail">
                    <?php if ($this->options->RandomPicChoice !=='0' && array_key_exists('thumb',unserialize($this->___fields()))): ?>
                        <img class="img-responsive" src="<?php echo $this->fields->thumb; ?>"/>
                    <?php else: ?>
                        <?php $thumb = showThumbnail($this); if(!empty($thumb)): ?>
                            <img class="img-responsive" src="<?php echo $thumb ?>"/>
                        <?php endif; ?>
                    <?php endif; ?>
                </a>
            </div>
            <div class="col-lg-9">
                <div class="post_content_preview">
                    <?php $this->excerpt(200, '...'); ?>
                </div>
            </div>
        </div>
        <div class="clearfix topic-footer">
            <span class="pull-right post_time"><?php $this->date('F j, Y'); ?></span>
        </div>
    </article>
<? endwhile ?>

他是怎么实现的,我能不能也这么干

先看看这个next()方法都是怎么使用的吧,整个项目目录搜索"->next("。interesting,果然看到了一种用法

// theme/default/sidebar.php  16行
<ul class="widget-list">
    <?php $this->widget('Widget_Comments_Recent')->to($comments); ?>
    <?php while($comments->next()): ?>
        <li><a href="<?php $comments->permalink(); ?>"><?php $comments->author(false); ?></a>: <?php $comments->excerpt(35, '...'); ?></li>
    <?php endwhile; ?>
</ul>

这段代码的大致意思很明显,$this->widget('Widget_Comments_Recent')加载了一个widget获得数据,然后用to方法赋值给$comments。接着我们就可以用$comments来做遍历输出了。

现在出现了两个关键函数next()和to(),继续全项目搜索,发现这俩方法都是在Widget基类中定义的。

var/Typecho/Widget.php

/**
 * 将类本身赋值
 *
 * @param string $variable 变量名
 * @return self
 */
public function to(&$variable)
{
    return $variable = $this;
}

/**
 * 返回堆栈每一行的值
 *
 * @return array
 */
public function next()
{
    if ($this->stack) {
        $this->row = @$this->stack[key($this->stack)];
        next($this->stack);
        $this->sequence ++;
    }

    if (!$this->row) {
        reset($this->stack);
        if ($this->stack) {
            $this->row = $this->stack[key($this->stack)];
        }

        $this->sequence = 0;
        return false;
    }

    return $this->row;
}

/**
 * 返回堆栈是否为空
 *
 * @return boolean
 */
public function have()
{
    return !empty($this->stack);
}

/**
 * 将每一行的值压入堆栈
 *
 * @param array $value 每一行的值
 * @return array
 */
public function push(array $value)
{
    //将行数据按顺序置位
    $this->row = $value;
    $this->length ++;

    $this->stack[] = $value;
    return $value;
}

上面的代码还有注释,意思也很明显了。Widget提供了一系列对内部数据进行操作的函数。have,是否有对象;next,下一个对象;to,赋值到一个变量;push,添加一个对象。

到这里,我们就有了一种很简单的实现遍历方式。functions.php中定义一个方法,先创建一个widget对象,通过push方法把查询结果添加进去,最后返回widget对象。模板中调用此方法得到一个widget对象,而后have to next随我们用了。

动手

我们试着把d8模板中获取热帖的方法hotpost()修改成上述方式实现。不废话上代码

// 首页置顶
function hotpost() {
    // 获得数据,来自D8模板
    $options = Typecho_Widget::widget('Widget_Options');
    if ((!empty($options->hot)) && floor($options->hot)==$options->hot) {
        $tjids =  $options->hot;
    }
    $hot_arr = explode(",", $tjids);
    if(count($hot_arr) < 2){
        $hot_arr[] = 1;
    }
    $db = Typecho_Db::get();    
    $results = $db->fetchAll(
        $db->select()->from('table.contents')
            ->orWhere('cid = ?',$hot_arr[0])
            ->orWhere('cid = ?', $hot_arr[1])
            ->where('type = ? AND status = ? AND password IS NULL', 'post', 'publish')
    );
    rsort( $results );//对数组逆向排序,即大ID在前

    // !!!! 创建我们的Widget对象
    $widget = Typecho_Widget::widget('Widget_Abstract_Contents');
    foreach ($results as $result) {
        // 数据处理
        $result['title'] = htmlspecialchars($result['title']);
        $result['permalink'] = htmlspecialchars($result['permalink']);
        $result['created'] = date('M d,Y', $result['created']);
        $result['views'] = number_format($result['views']);
        preg_match_all( "/\[.*?\]:\s*(http(s)?:\/\/.*?(jpg|png|gif))/i", $result['text'], $matches );
        if(isset($matches[1][0])){
            $result['thumb'] = $matches[1][0];
        }else{
            $result['thumb']= $options->themeUrl .'/images/sj/' . rand(1, 5) . '.jpg';
        }

        // !!!! push到widget里面去
        $widget->push($result);
    }

    // 返回widget对象
    return $widget;
}

模板代码修改

<div class="row hots">
        <!--导出$hot变量--->
    <?php hotpost()->to($hot); ?>

        <!--$hot变量是否包含对象,没有不输出对吧--->
    <?php if ($hot->have()): ?>
        <h2>Hot</h2>
                <!--hot->next()遍历有没有 。。。。-->
        <?php while ( $hot->next() ) : ?>
            <div class="col-lg-4 col-md-4 col-sm-6 col-xs-12">
                <a href="<?php $this->permalink() ?>" class="post_thumbnail">
                    <?php if ($this->options->RandomPicChoice !=='0' && array_key_exists('thumb',unserialize($this->___fields()))): ?>
                        <img class="img-responsive" src="<?php echo $this->fields->thumb; ?>"/>
                    <?php else: ?>
                        <?php $thumb = showThumbnail($this); if(!empty($thumb)): ?>
                            <img class="img-responsive" src="<?php echo $thumb ?>"/>
                        <?php endif; ?>
                    <?php endif; ?>
                </a>
                <p class="title"><a target="_blank" itemtype="url" href="<?php $this->permalink() ?>"><?php $hot->title() ?></a></p>
            </div>
        <?php endwhile ?>
    <?php endif ?>
</div>

最后来个效果截图
WeChat Image_20180821122220.png

========================================================================================

后续:修复bug

后台设置了三篇hotpost,分别cid=1,5,24。然而前台只显示了1和5,24没显示。回过头重新看一下hotpost()的实现,也是惊呆了,居然还可以这么搞。

// 获得数据,来自D8模板
$options = Typecho_Widget::widget('Widget_Options');
if ((!empty($options->hot)) && floor($options->hot)==$options->hot) {
    $tjids =  $options->hot;
}
$hot_arr = explode(",", $tjids);
if(count($hot_arr) < 2){
    $hot_arr[] = 1;
}
$db = Typecho_Db::get();    
$results = $db->fetchAll(
    $db->select()->from('table.contents')
        ->orWhere('cid = ?',$hot_arr[0])
        ->orWhere('cid = ?', $hot_arr[1])
        ->where('type = ? AND status = ? AND password IS NULL', 'post', 'publish')
);

我们来一段一段仔细看这短短10来行代码

$options = Typecho_Widget::widget('Widget_Options');
if ((!empty($options->hot)) && floor($options->hot)==$options->hot) {
    $tjids =  $options->hot;
}

var_dump($hot_arr)可以看到$hot_arr = [1,5,24],说明后台设置以及数据库数据都正常。

接着往下看这段

if(count($hot_arr) < 2){
    $hot_arr[] = 1;
}

当后台设置hotpost少于两篇的时候,系统会自动增加cid=1文章,防止hotpost出现空的情况发生。
问题是如果我删除了cid=1的文章会怎样?
修改:没数据就不显示hotpost呗,既然不设置说明不需要

接着往下看

$db = Typecho_Db::get();    
$results = $db->fetchAll(
    $db->select()->from('table.contents')
        ->orWhere('cid = ?',$hot_arr[0])
        ->orWhere('cid = ?', $hot_arr[1])
        ->where('type = ? AND status = ? AND password IS NULL', 'post', 'publish')
);

简直坑爹有没有,不管你怎么设置,他就只取得前两篇。后台设置和前台展示严重不匹配

另外,我发现不需要手动创建Widget对象,可以通过下面方式获取,以及push操作

// 获得widget对象
$widget = Typecho_Widget::widget('Widget_Abstract_Contents');

// widge的push方法增加文章,增加文章操作会自动生成$result['permlink']
// $result['permlink'] 是 null
$result = Typecho_Widget::widget('Widget_Abstract_Contents')->push($result);
// $result['permlink']包含文章url

解决方案:
重写hotpost()函数

function hotpost() {
    $options = Typecho_Widget::widget('Widget_Options');
    if ((!empty($options->hot)) && floor($options->hot)==$options->hot) {
        $tjids =  $options->hot;
    }

    $hot_arr = explode(",", $tjids);
    if (count($hot_arr) > 0) {
        $db = Typecho_Db::get();

        $query = $db->select()->from('table.contents');
        foreach ( $hot_arr as $cid ){
            $query->orWhere('cid = ?', $cid);
        }
        $query->where('type = ? AND status = ? AND password IS NULL', 'post', 'publish');

        $results = $db->fetchAll($query);

        rsort( $results );//对数组逆向排序,即大ID在前

        foreach ($results as $result) {
            // 数据处理
            $result['title'] = htmlspecialchars($result['title']);
            $result['created'] = date('M d,Y', $result['created']);
            $result['views'] = number_format($result['views']);
            preg_match_all( "/\[.*?\]:\s*(http(s)?:\/\/.*?(jpg|png|gif))/i", $result['text'], $matches );
            if(isset($matches[1][0])){
                $result['thumb'] = $matches[1][0];
            }else{
                $result['thumb']= $options->themeUrl .'/images/sj/' . rand(1, 5) . '.jpg';
            }

            // 加入widget
            $result = Typecho_Widget::widget('Widget_Abstract_Contents')->push($result);

            // 再加入widget之前,$result['permalink']没有数据,然而加进去了我在对url进行htmlspecialchars处理?逻辑不大对
            //var_dump($result['permalink']);
            //$result['permalink'] = htmlspecialchars($result['permalink']);
        }
    }

    return Typecho_Widget::widget('Widget_Abstract_Contents');
}

现在好了,总算是正常了。

打赏

微信打赏

支付宝打赏



发表我的评论
'
昵称 (必填)
邮箱 (必填)
网址