Tag: bootstrap

  • 任氏有无轩最新改版

    任氏有无轩改版已经很多次了,从最早的Symfony 1.x开始,在Symfony框架进入到2.x的时候重新改版了一次,最近的这次刚刚结束,也算是很大的一次。

    根据我在Bitbucket的提交记录,3月12日开始第一次重新提交,4月22日完成最后一次的提交,历时40余天。但是,众所周知,我的开发是很断续的,平均下来每天投入的时间不会超过1个小时,所以这次改版的总耗时不超过40小时,也就是常规工作时间下一周的工作量——加班的话也就3天左右。

    这次的改版牵涉面非常广。

    前端我还是使用Symfony 2来搭建我的应用,这个框架我已经用了很多年,如果没有什么大的变动和根本性的原因,我应该不会轻易切换别的框架——比如这个帖子里提到的Laraval和Phalcon。

    我原来的站点是用Bootstrap自己搭建的。优点是没有约束,自由度大,而且Bootstrap提供的部件已经给出了足够的基础来进行这个工作;缺点是我不是设计人员,设计出来的页面总是脱不了Bootstrap本身提供的一些模板。所以,下定决心花了$4在wrapbootstrap.com买了一个商业模板Grove(顺大便说一句,这个模板涨到$12了)。

    用商业模板的好处是,总可以找到适用的基本页面来改造;缺点是,很依赖它提供的样式。不过总的来说,利大于弊,我觉得我这个$4花得很值得。 另外,也许是我运气好,这个模板还提供了一个3D/2D幻灯片过渡的库,让我轻松做出很炫的Carousel场景切换效果。

    最后,我还加入了长久以来一直缺失的按照书籍tag搜索书籍的功能。下一步,我还想加入一个tag云。

    后端**后端是这次改动最大的地方。**

    首先,数据库结构重新编写。

    在我给Sitepoint写的文章中,我明确表示:我是很反对使用一个自增长的整数字段来作为主键(Primary Key)的。但是,为了进行Symfony 2下的数据自动填充和另外一个后台(还没有实现)Phreeze的要求,我不得不重新构造我的数据库为那些原来没有自增长字段PK的表格重新加入这样的一个主键。数据迁移工作也花费了不少重复性的工作。

    第二,部分前端的动态内容提供,我开始采用Dart,而不是之前的纯jQuery。使用Dart,我的开发速度大大加快。虽然有一些限制——比如我这个帖子中提到的,但是总体来说利大于弊,而且我作为曾经在苏州Google Developer Group组织的Fly Dart活动中主讲过Dart的人,当然那更应该以身作则,多使用一些Dart。

    第三,我整合了Wordpress的一些功能,比如获得最新的N个帖子。

    总的来说,这次的改造是非常成功的,部署也很顺利。感觉自己对Symfony 2的掌握又深入了一层。 放张首页的效果图(点击可看大图):

    Ashampoo_Snap_2014.04.23_19h33m20s_001_Chrome

  • 任氏有无轩改版彻底完成

    折腾了很久,终于将我的任氏有无轩改版到4.0。

    这次的改版经历了很长时间。

    首先是用Symfony 2.X,这对我是个挑战,因为我之前都是基于Symfony 1.4.X来编写站点的。Symfony的一个最大的特点就是每次大的版本升级都会引入全新的东西,需要开发者去适应,这是闹哪套!

    Symfony 2首先是基于namespace,因此必须有PHP 5.3+的版本支持。我的主机在BlueHost上,应该说BH是很保守的虚拟主机服务商,在外界已经普及使用PHP 5.3的时候,它还死撑着用PHP 5.2。我还为此专门去信询问,回复是他们还要测试。没有这个最基本的平台,我的开发也就没有了动力。

    幸好,BH主机从善如流,一口气开始支持PHP 5.3/5.4,所以我才又重拾开发。

    其次是版面的设计。这要感谢前令狐,在7月份的第一届G4PCC上,他介绍了Bootstrap这个框架。这个框架的存在让我精神一振,因为它可以让一个程序员(而不是美工师)设计出比较优雅、简介的页面。通过简单的摸索,我就已经上手了。

    第三,SF2开始使用Twig模板引擎。这个和之前的基于PHP的引擎完全不同。以前在基于PHP的模板引擎中,可以随时插入PHP代码,但是现在不行,必须要么通过变量传递,要么用Twig内置的所谓filter。当然,Twig引擎本身是非常非常好用的。它支持模板嵌套,支持控制器嵌套——这点至关重要。

    第四,SF2使用的数据库ORM也统一到Doctrine。这个引擎,说实话我到现在也还没有完全掌握。但是确实很好很强大,特别是它的反向工程能力,可以将一个现有的数据库——我的应用中就是这个情形——比较完整的反向映射到mapping,并生成对应的class/Entity。同时,它也支持所谓的Repository,可以统一数据库的操作。很好的实现了M层。

    第五,SF2使用了composer.phar这个鬼东西来管理升级。不知何故,当我进行更新时,总是将DEV版本(而不是Stable版本)拖下来。SF2由于开发速度太快,DEV版本总是有不少bug。而其中的一个bug是,prod环境下第一次生成cache的时候是大小写不敏感的,但是在以后的prod环境下的调用时却是大小写敏感的。因为这个问题,我的站点在发布后出现了很奇怪的问题:我的根目录路径在第一次进入时OK,但是在随后的进入时就会引起500错误。我调了很多次都没有能找到问题,最后是人工降SF2的版本并在dev环境下捉出了这个问题。

    前几天猛禽在G+上说,Drupal也决定将采用SF2作为框架。我对此倒是不意外。

  • 重构“任氏有无轩”——第三天

    今天继续加深书籍详细信息页面的构造。

    在G+上95对我的进展发了一个评论:

    再加上点自动抓取网上共享章节的功能

    对这个要求,我只能说我只能实现一点点。我将在详细信息页面中构造一个显示豆瓣对应书籍的信息的部分。

    另外,我要实现一个功能是在这个页面中编辑书籍tags的功能。

    先看实现的界面:

    detail

    先讲在页面中编辑书籍tags。

    这个其实很简单,在控制器中创建一个动作tagaddAction,用来处理“增加更多TAG”按钮的POST动作:

    public function tagaddAction(Request $req)
    {
        $q=$req->request->all();
        $q['newtags'];
        $id=$q['id'];
        $this->processTagAdd($tags, $id);
        $url=$this->get('router')->generate('book_detail', array('id'=>$id));
        return $this->redirect($url);
    }
    private function processTagAdd($tags, $id)
    {
        $em=$this->getDoctrine()-gt;getEntityManager();
        $existing=$em->getRepository('trrsywxBundle:BookBook')->getTagsByBookId($id);
        $existing=$this->convertTagsToArray($existing);
        $book=$em->getRepository('trrsywxBundle:BookBook')->findOneBy(array('id'=>$id));
        $tags=trim($tags);
        $tagslist=  explode(' ', $tags);
        foreach ($tagslist as $tag)
        {
            if(!in_array($tag, $existing)) //This is a new tag
            {
                $booktaglist=new trrsywxBundleEntityBookTaglist();
                $booktaglist->setId($book);
                $booktaglist->setTag($tag);
                $em->persist($booktaglist);
            }
            $em->flush();
        }
    }

    这里有几个需要讲一讲的地方:

    • 在控制器中生成一个url,然后重定向。这也是一般表单post后所需要进行的最后一步。
    • 在processTagAdd中,需要注意$booktaglist->setId()的参数不是$book.id,而是$book本身。所以,虽然我传递进去的是$id,但是之前不得不用一个Repository函数来找出这本书。
    • 注意一个新的taglist是怎样创建,赋值,持续化的。最后,将所有持续化后的对象一次性flush到数据库中去。
    • 用in_array来判断要新加的tag是否已经存在于原来的taglist里,同时要将taglist对象中的tag提取出来变成一个数组——因为它本来是个对象数组。这个是由一个辅助函数convertTagsToArray完成的。

    =====我是分割线=====

    接下来讲一讲获得豆瓣的信息。

    我用到了Snoopy这个库,关于如何在Symfony 2中加入第三方库的说明,参见这篇文章

    获得豆瓣信息的核心代码如下:

    private function getDoubanRemote($isbn)
    {
            $bad_isbn = 'bad isbn';
            $url = "http://api.douban.com/book/subject/isbn/$isbn?alt=json";
            $s = new SnoopySnoopy();
            $s->agent = 'RSYWX.net - http://www.rsywx.net';
            $s->read_timeout = 1;
            //$s->user = 'taylor.ren@gmail.com';
            //$s->pass = 'xxxxxx';
            $s->fetch($url);
            $res = $s->results;
            if ($res == $bad_isbn) // My isbn is not found in douban
                return false;
            else
                return json_decode($res, true);
    }
    public function getDouban($isbn)
    {
            $ret = $this->createDummyReturn();
            $res = $this->getDoubanRemote($isbn);
            if (!$res) // The above call not successful
            {
                return $ret;
            } else
            {
                if (array_key_exists('summary', $res))
                    $summary = $res['summary']['$t'];
                else
                    $summary = '(豆瓣还没有简介)';
                $ret['summary'] = $summary;
                $ret['alternate'] = $res['link'][1]['@href'];
                if (array_key_exists('db:tag', $res))
                    $tags = $res['db:tag'];
                else
                    $tags[] = '豆瓣没有给出任何TAG';
                $ret['tags'] = array(); //Clear up the dummy tag 'not found';
                foreach ($tags as $t)
                {
                    $ret['tags'][] = $t['@name'];
                }
                $rating = $res['gd:rating']['@average'];
                if ($rating == 0)
                    $rating = '(还没有评分)';
                $ret['rating'] = $rating;
            }
            return $ret;
    }

    其实没有什么技术含量,主要是从豆瓣API返回的数组中提取我用得着的数据而已。

    到此,书籍详细页面已经完成。我接下来要做的是书籍列表部分。这个部分需要用到分页。可耻的是,Symfony 2没有内置分页部件,我需要自己来编写。这个会是一个挑战。

    【本文收录于[go4pro.org]】

  • 重构“任氏有无轩”——第二天

    重构进入第二天——熟悉我风格的人都知道,这不是真正的第二天。

    今天主要进行书籍详细信息页面的创建,以及对书籍封面的处理。

    页面的创建继续使用Bootstrap来完成,用到的元素包括Hero Unit,Table等。我这里不进行详细的展开。

    相较之前的页面,我准备在新的页面中加入书籍的封面。这里的一个问题是:不是所有我收藏的书我都扫描了封面——以后也许会的,但是目前的话,我觉得用一个类似缺省封面的设计是比较好的。

    缺省封面当然不能将每本书的信息都录入,因此需要动态生成。同样的,即使有书籍本身的封面,我也要生成一些版权信息。所幸的事,这段代码大部分已经在我之前的编程中实现,因此本次只是进行修改并配合Symfony 2的框架。

    首先创建一个路由,这个路由只有一个功能,就是将形如:/bookseller/cover/id_title_author.png这样的请求映射给恰当的控制器,并由控制器来完成缺省封面的动态生成,然后返回一个png图形的句柄,再由浏览器进行渲染:

    cover:
        pattern: books/cover/{id}_{title}_{author}.png
        defaults: {_controller: trrsywxBundle:Default:cover}

    然后修改模板文件,引用到<img ...="..." />的地方都要改写:

    <img src="{{path("cover", {'id':book.id, 'author':book.author, 'title':book.title}) }}" alt="{{book.title}}的封面" title="{{book.title}}的封面"/>

    最后,当然是控制器的编写,主要是用到PHP内置的GD库的函数,我这里不展开。

    我将default.jpg和某本书对应的真正封面(如果有)放在一个目录里,如果找到和书籍对应的封面(根据文件名判断),那么显示这个封面并加上版权信息;如果没有对应的封面,那么显示缺省封面,加上书名、作者以及版权信息等。

    以下是效果:

    default cover

    这样做,当然有副作用:每个图片都需要动态调入,会有额外的开销。

    【本文收录于[go4pro.org]】

  • 重构“任氏有无轩”——第一天

    按照惯例,这个“第一天”其实是假的:在我而言,利用零零碎碎的时间完成了“第一天”——即一个比较专注于开发这个项目的人可以在一天,甚至半天时间内完成——的内容。

    对于Symfony 2框架,我就不准备多介绍了。一言以蔽之,这是一个全面而强大的PHP框架。

    这次重构有两个重要的目的:

    首先是熟悉Symfony 2框架(我之前的开发是基于Symfony 1.4版本),其次是学习一些新的东西(比如Bootstrap)。

    闲话不说,开始!

    我的站点根目录在g:/www/rsywx2,然后Symfony框架会安装在Symfony目录下,这个目录也是我们日常工作的目录,以下的命令,如果不特别之处,都是在这个目录下运行。 另外,Apache的配置中,要将DocumentRoot设置到g:/www/rsywx2/Symfony/web下。 我下载的是所谓没有Vendor的文件包,所以解压后需要运行一个命令来安装各种预设的包:

    php binvendors install这个命令需要git的支持。它会解析deps和deps.lock文件,安装各种基本的包。

    安装完毕后,目录结构如下:

    symfony2-tree

    我这里不展开描述各个目录的功能。在以后的编程中会不断提到,大家自然就会不断熟悉。

    此时,我们可以用http://localhost/app_dev.php来浏览一个示范界面。这个界面我们会很快就替换掉的。

    下一步就是开始进行第一个Bundle的创建。Symfony 2中用Bundle来描述一个应用,这个Bundle中包含了Controller,Entity,Repository,Resource,Tests等内容。简单描述如下:

    • Controller:MVC中的C部分,所有的程序逻辑都将在这个目录中的文件里实现。
    • Entity:反应了从PHP类到数据库表格之间的映射关系,并且加入了大量的getter/setter,以及Relationship的描述。
    • Resource:主要是两部分。一部分是所有的View,可以简单的理解为Web页面,Symfony 2推荐使用Twig引擎来描述页面。这个引擎也是由Symfony 2的开发者开发的。另一部分是一些config。如果你像我一样,习惯根据数据库来创建ORM,那么这里会有数据库中各个表的描述——它和Entity不同,在我的用例中,Entity是要根据这个来创建的。
    • Tests:可以用来放置测试文件。

    创建Bundle是向导式的,输入如下命令即可:

    php appconsole generate:bundle

    你需要输入一些Bundle的基本信息,Symfony会为你创建对应的一个Bundle。注意:所有的Bundle都要以Bundle结束,可以加入namespace的分割。

    创建好的一个Bundle结构如上述。

    下来就是设置数据库,修改app/config/parameter.ini文件,设置数据库、数据库类型、用户、密码等信息。当然,在我的用例中,我是先创建好数据库、表格的,所以数据库、表格已经全部存在了,我们需要导入这些信息:

    php appconsole doctrine:mapping:importphp appconsole doctrine:generate:entities

    import的时候需要指定一下格式,可以用yml,xml,php等格式。第一次运行第二个命令时,需要指定一下path,即生成的Entities要放在哪里,一般只要指定path=/src即可。这样,生成的Entity的PHP文件会放在你创建的Bundle的Entity目录下。 至此,数据库建模已经全部完成。

    MVC模块中,M部分是提供数据的。我们当然可以在Controller中直接查询,但是为了更好的复用模块,我们可以创建一个Repository,将所有数据库相关的操作全部放在里面。但是,由于Doctrine本身有自己的Repository,我们必须指定我们自己的Repository来替换某个Entity缺省的Repository:

    //修改srctrrsywxBundleResourcesconfigdoctrineBookReview.orm.xml文件
    //这里需要加以必要的修改以匹配你import进来的文件和你的namespace和Bundle名
    
    <entity name=trrsywxBundleEntityBookReview repository-class=trrsywxBundleRepositoryBookRepository table=book_review>

    重新运行一下doctrine:generate:entities,如果之前没有Repository目录,就会创建这个目录,并在目录下创建一个BookRepository文件。

    再简单讲一下Twig模板引擎。

    Twig很简单。要么用{{…}}表示输出,要么用{%…%}表示控制结构。同时它支持嵌套、继承,甚至可以在模板中嵌入一个控制器。

    最后简单讲一下Bootstrap。它简明、优雅,适合那些程序员来写界面。

    第一天的成果大致如下,请勿见笑:

    symfony2-day1-index

    【本文收录于[go4pro.org]