<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Symfony中文教程 &#187; symfony2.6</title>
	<atom:link href="http://www.newlifeclan.com/symfony/archives/tag/symfony2-6/feed" rel="self" type="application/rss+xml" />
	<link>http://www.newlifeclan.com/symfony</link>
	<description>站在巨人肩膀上的phpweb框架</description>
	<lastBuildDate>Fri, 12 Dec 2025 00:58:27 +0000</lastBuildDate>
	<language>zh-CN</language>
		<sy:updatePeriod>hourly</sy:updatePeriod>
		<sy:updateFrequency>1</sy:updateFrequency>
	<generator>https://wordpress.org/?v=4.0.38</generator>
	<item>
		<title>KU案例2之10给数据库中的用户添加角色</title>
		<link>http://www.newlifeclan.com/symfony/archives/294</link>
		<comments>http://www.newlifeclan.com/symfony/archives/294#comments</comments>
		<pubDate>Thu, 09 Apr 2015 14:37:48 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[实战教程]]></category>
		<category><![CDATA[role]]></category>
		<category><![CDATA[security]]></category>
		<category><![CDATA[symfony2.6]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=294</guid>
		<description><![CDATA[<p>很多时候，所有的用户只能得到一个角色：ROLE_USER，这是因为我们的User entity中的getRol [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/294">KU案例2之10给数据库中的用户添加角色</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>很多时候，所有的用户只能得到一个角色：ROLE_USER，这是因为我们的User entity中的getRoles()的默认返回值所导致。</p>
<p><span id="more-294"></span></p>
<p>所以我们要在User entity中添加一个字段roles，并且把它设置成json_array类型：</p><pre class="crayon-plain-tag">// src/Acme/UserBundle/Entity/User.php
// ...

/**
 * @ORM\Column(type="json_array")
 */
private $roles = array();</pre><p>json_array()允许我们存储一个字符串数组到一个字段里。在数据库中，这些数组被存储为JSON字符串。Doctrine来负责array和JSON之间的转换工作。</p>
<p>现在我们要更新getRoles()方法，并添加setRoles方法：</p><pre class="crayon-plain-tag">public function getRoles()
{
    return $this-&gt;roles;
}

public function setRoles(array $roles)
{
    $this-&gt;roles = $roles;

    // allows for chaining
    return $this;
}</pre><p>很酷，但是现在的方式，用户的用户角色很可能为空。他们变成了僵尸用户，他们能够登录，但不能访问页面，我们不能让这种事情发生。</p>
<p>应该在getRoles()添加一些逻辑，来保证每个用户都有ROLE_USER。</p><pre class="crayon-plain-tag">public function getRoles()
{
    $roles = $this-&gt;roles;
    $roles[] = 'ROLE_USER';

    return array_unique($roles);
}</pre><p>更新一下数据库</p><pre class="crayon-plain-tag">php app/console doctrine:schema:update --force</pre><p>让我们添加一个ROLE_ADMIN角色的用户</p><pre class="crayon-plain-tag">// src/Acme/UserBundle/DataFixtures/ORM/LoadUsers.php
// ...

public function load(ObjectManager $manager)
{
    // ...
    $manager-&gt;persist($user);

    $admin = new User();
    $admin-&gt;setUsername('wayne');
    $admin-&gt;setPassword($this-&gt;encodePassword($admin, 'waynepass'));
    $admin-&gt;setRoles(array('ROLE_ADMIN'));
    $manager-&gt;persist($admin);

    $manager-&gt;flush();
}</pre><p>运行下面命令添加数据</p><pre class="crayon-plain-tag">php app/console doctrine:fixtures:load</pre><p>现在我们用管理员登陆，debug条会显示我们有ROLE_USER和ROLE_ADMIN角色。</p>
<h2>使用AdvancedUserBundle禁用不活跃用户</h2>
<p>我们可以对一些不怎么来网站的用户进行禁用操作。</p>
<p>添加一个isActive的boolean字段到User entity。如果这个字段为假，那么程序会阻止用户进行身份验证。不要忘了在命令行运行 doctrine:generate:entities 来生成get和set方法：</p><pre class="crayon-plain-tag">/ src/Acme/UserBundle/Entity/User.php
// ...

/**
 * @var bool
 *
 * @ORM\Column(type="boolean")
 */
private $isActive = true;

// ...
// write or generate your getIsActive and setIsActive methods...</pre><p>在那之后，更新我们的数据库字段</p><pre class="crayon-plain-tag">php app/console doctrine:schema:update --force</pre><p>现在isActive字段已经存在了，但是它还不能的在登陆中使用。为了使他能工作，要将User类的UserInterface替换成implements AdvancedUserInterface；</p><pre class="crayon-plain-tag">// src/Acme/UserBundle/Entity/User.php
// ...

use Symfony\Component\Security\Core\User\AdvancedUserInterface;

class User implements AdvancedUserInterface
{
    // ...
}</pre><p>这里 AdvancedUserInterface继承UserInterface。</p>
<p>新的接口是一个更强大的UserInterface他需要四个额外的方法。如果这些方法返回false，Symfony将阻止用户登录。为了证明这一点，除了isAccountNonLocked都返回true：</p><pre class="crayon-plain-tag">// src/Acme/UserBundle/Entity/User.php
// ...

public function isAccountNonExpired()
{
    return true;
}

public function isAccountNonLocked()
{
    return false;
}

public function isCredentialsNonExpired()
{
    return true;
}

public function isEnabled()
{
    return true;
}</pre><p>现在登录不太好玩了：我们堵住了有用的信息。</p>
<p>这里所有的方法都在做同样的事情：就是阻止登陆。如果你想你可以翻译每个用户带来的不同信息。设置每一个都为true，除了isEnabled。让他返回isActive属性的值：</p><pre class="crayon-plain-tag">/ src/Acme/UserBundle/Entity/User.php
// ...

public function isAccountNonLocked()
{
    return true;
}

public function isEnabled()
{
    return $this-&gt;getIsActive();
}</pre><p>如果isActive为false，这应该阻止用户登录。</p>
<p>我们更改不活跃的用户：</p><pre class="crayon-plain-tag">// src/Acme/UserBundle/DataFixtures/ORM/LoadUsers.php
// ...

public function load(ObjectManager $manager)
{
    // ...
    $admin-&gt;setIsActive(false);
    // ...
}</pre><p>运行命令：</p><pre class="crayon-plain-tag">php app/console doctrine:fixtures:load</pre><p>当我们尝试登录，程序自动阻止。爽！</p>
<p>哈哈，学会了吧。我们可以还原程序了，让用户能够登录进来。</p>
<p>&nbsp;</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/294">KU案例2之10给数据库中的用户添加角色</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/294/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>jobeet第六天:更多的数据模型</title>
		<link>http://www.newlifeclan.com/symfony/archives/347</link>
		<comments>http://www.newlifeclan.com/symfony/archives/347#comments</comments>
		<pubDate>Fri, 27 Mar 2015 05:40:55 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[实战教程]]></category>
		<category><![CDATA[jobeet]]></category>
		<category><![CDATA[symfony2.6]]></category>
		<category><![CDATA[symfony路由]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=347</guid>
		<description><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的Jobeet Tutiru [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/347">jobeet第六天:更多的数据模型</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>*这一系列文章来源于Fabien Potencier，基于Symfony1.4编写的<a href="http://symfony.com/legacy/doc/jobeet?orm=Doctrine">Jobeet Tutirual</a>。</p>
<h2><a id="user-content-doctrine查询对象" class="anchor" href="https://github.com/happen-zhang/symfony2-jobeet-tutorial/blob/master/chapter-06/chapter-06.md#doctrine%E6%9F%A5%E8%AF%A2%E5%AF%B9%E8%B1%A1"></a><em>Doctrine</em>查询对象</h2>
<p>在第二天的内容中我们定义了这样一个需求（requirements）：“在<em>Job</em>首页显示最近发布的和在有效期内的<em>Job</em>信息列表”。我们现在在首页中显示的是数据库中全部的<em>Job</em>数据，而没有考虑到不需要在首页中显示已过期的<em>Job</em>信息。</p>
<p><span id="more-347"></span></p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

class JobController extends Controller
{
    public function indexAction()
    {
        $em = $this-&gt;getDoctrine()-&gt;getManager();

        $entities = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;findAll();

        return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(
            'entities' =&gt; $entities
        ));

 // ...
}</pre><p>一个在有效期限内的<em>Job</em>数据就代表着这个<em>Job</em>信息发布的日期在30天之内。<code>$entities = $em-&gt;getRepository('IbwJobeetBundle')-&gt;findAll()</code>这行代码是从数据库中取出所有的<em>Job</em>数据，因为我们并没有给查询指定任何的条件。</p>
<p>现在我们来做些修改，我们希望取出的是在有效期内的<em>Job</em>数据：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
public function indexAction()
{
    $em = $this-&gt;getDoctrine()-&gt;getManager();

    $query = $em-&gt;createQuery(
        'SELECT j FROM IbwJobeetBundle:Job j WHERE j.created_at &gt; :date'
    )-&gt;setParameter('date', date('Y-m-d H:i:s', time() - 86400 * 30));
    $entities = $query-&gt;getResult();

    return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(
        'entities' =&gt; $entities
    ));
}</pre><p>&nbsp;</p>
<h2>调试<em>Doctrine</em>生成的<em>SQL</em></h2>
<p>有时候查看<em>Doctrine</em>生成出来的<em>SQL</em>对我们调试<em>Bug</em>是很有帮助的。比如，我们可能在使用<em>Doctrine</em>进行数据库查询时得不到我们所期望的结果时，那么我们就很需要去查看<em>Doctrine</em>生成的<em>SQL</em>，并以此来对我们的代码进行排错。在开发环境（development）下，还好有<em>Symfony</em>的调试工具栏（浏览器页面的最下面的那条栏），我们能在调试栏中找到大量我们在调试过程中需要用到的信息，包括刚才所说的<em>SQL</em>调试（http://jobeet.local/app_dev.php）。</p>
<p><img class="alignnone size-large wp-image-348" src="http://www.newlifeclan.com/symfony/wp-content/uploads/sites/2/2015/03/06-01-1024x560.png" alt="06-01" width="780" height="426" /></p>
<h2>序列化对象</h2>
<p>尽管上面的代码可以运行了，但这离完美还差很远呢，因为我们还没有考虑到另一个需求：“一个用户能够重新激活或者延续<em>Job</em>信息的期限多30天”。</p>
<p>在上面的代码中我们只依赖了<em>created_at</em>的值来取出不过期的<em>Job</em>数据，由于<em>created_at</em>代表的是<em>Job</em>的创建时间，如果我们还需要表示<em>Job</em>数据的过期时间，我们还要另外一些列（columns）。</p>
<p>如果你还记得我们在<a href="http://www.newlifeclan.com/symfony/archives/328" target="_blank">第三天</a>的内容中描述的数据表结构的话，你肯定会记起我们还定义了一个<em>expires_at</em>列。由于我们还没在<em>Fixture</em>文件中设置<em>expires_at</em>的值，所以它们的值都是空的。我们需要的是，当一个<em>Job</em>被创建的时候，<em>expires_at</em>的值就会被设置成<em>created_at</em>值的30天之后。</p>
<p>每当我们需要在<em>Doctrine</em>对象被序列化到数据库中之前能够自动执行一些操作的时候，我们可以添加一个新的行为（action）到<em>ORM</em>映射文件的<em>lifecycle callback</em>区块中，就像我们之前对<em>created_at</em>列进行的操作：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/doctrine/Job.orm.yml
# ...
    # ...
    lifecycleCallbacks:
        prePersist: [ setCreatedAtValue, setExpiresAtValue ]
        preUpdate: [ setUpdatedAtValue ]</pre><p>现在我们需要重新生成实体类（entity class），这样Doctrine就会在Job实体类中添加一个<em>setExpiresAtValue()</em>函数：</p><pre class="crayon-plain-tag">php app/console doctrine:generate:entities IbwJobeetBundle</pre><p>打开<em>src/Ibw/JobeetBundle/Entity/Job.php</em>文件，我们来编辑<em>setExpiresAtValue()</em>函数：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Job.php
// ...

class Job
{
    // ... 

    public function setExpiresAtValue()
    {
        if(!$this-&gt;getExpiresAt()) {
            $now = $this-&gt;getCreatedAt() ? $this-&gt;getCreatedAt()-&gt;format('U') : time();
            $this-&gt;expires_at = new \DateTime(date('Y-m-d H:i:s', $now + 86400 * 30));
        }
    }
}</pre><p>好，现在让我们来用<em>expires_at</em>列来替换<em>created_at</em>列来取出未过期的<em>Job</em>数据：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

    public function indexAction()
    {
        $em = $this-&gt;getDoctrine()-&gt;getManager();

        $query = $em-&gt;createQuery(
            'SELECT j FROM IbwJobeetBundle:Job j WHERE j.expires_at &gt; :date'
    )-&gt;setParameter('date', date('Y-m-d H:i:s', time()));
        $entities = $query-&gt;getResult();

        return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(
            'entities' =&gt; $entities
        ));
    }

// ...</pre><p>&nbsp;</p>
<h2>加入更多的<em>Fixtures</em></h2>
<p>现在我们刷新浏览器中的<em>Job</em>首页，我们不会看到页面有任何的改变，因为我们之前加入到数据库中的<em>Job</em>数据都才仅仅发布了几天，所以现在数据库中的<em>Job</em>数据都是在有效期内的。让我们在<em>Fixture</em>文件中加入一些已过期的（<em>expired</em>）的<em>Job</em>数据吧：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/DataFixtures/ORM/LoadJobData.php
// ...

    public function load(ObjectManager $em)
    {
        $job_expired = new Job();
        $job_expired-&gt;setCategory($em-&gt;merge($this-&gt;getReference('category-programming')));
        $job_expired-&gt;setType('full-time');
        $job_expired-&gt;setCompany('Sensio Labs');
        $job_expired-&gt;setLogo('sensio-labs.gif');
        $job_expired-&gt;setUrl('http://www.sensiolabs.com/');
        $job_expired-&gt;setPosition('Web Developer Expired');
        $job_expired-&gt;setLocation('Paris, France');
        $job_expired-&gt;setDescription('Lorem ipsum dolor sit amet, consectetur adipisicing elit.');
        $job_expired-&gt;setHowToApply('Send your resume to lorem.ipsum [at] dolor.sit');
        $job_expired-&gt;setIsPublic(true);
        $job_expired-&gt;setIsActivated(true);
        $job_expired-&gt;setToken('job_expired');
        $job_expired-&gt;setEmail('job@example.com');
        $job_expired-&gt;setCreatedAt(new \DateTime('2005-12-01'));

        // ...

        $em-&gt;persist($job_expired);
        // ...
    }

// ...</pre><p>现在我们来重新加载<em>Fixtures</em>，然后刷新浏览器，确保过期的<em>Job</em>信息不会被显示出来：</p><pre class="crayon-plain-tag">php app/console doctrine:fixtures:load</pre><p>&nbsp;</p>
<h2>重构代码</h2>
<p>尽管上面的代码已经能运行了，但我们所做得还是不够好，你能发现其中有什么问题吗？</p>
<p><em>Doctrine</em>的查询代码不应该属于<em>action</em>（<em>Controller</em>层），它应该属于<em>Model</em>层。在<em>MVC</em>模式中，<em>Model</em>层定义的是业务逻辑，而<em>Controller</em>层则通过是<em>Model</em>层来从数据库中取出数据。现在我们来把之前从数据库中获取<em>Job</em>数据集合的代码从<em>Controller</em>层移到<em>Model</em>层吧。现在我们为<em>Job</em>实体类创建一个<em>Repository</em>类。</p>
<p>打开<em>/src/Ibw/JobeetBundle/Resources/config/doctrine/Job.orm.yml</em>，添加下面的代码：</p><pre class="crayon-plain-tag">Ibw\JobeetBundle\Entity\Job:
    type: entity
    repositoryClass: Ibw\JobeetBundle\Repository\JobRepository
    # ...</pre><p>运行下面的命令，<em>Doctrine</em>能够帮我们生成<em>Repository</em>类：</p><pre class="crayon-plain-tag">php app/console doctrine:generate:entities IbwJobeetBundle</pre><p>下一步我们来给<em>JobRepository</em>类添加一个方法：<em>getActiveJobs()</em>。这个方法可以取出有效期内的<em>Job</em>数据，并且按照<em>expires_at</em>的值进行排序（它还可以接受一个<em>$category_id</em>参数，它可以按分类来取出<em>Job</em>数据）。</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Repository/JobRepository.php
namespace Ibw\JobeetBundle\Repository;

use Doctrine\ORM\EntityRepository;

/**
 * JobRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class JobRepository extends EntityRepository
{
    public function getActiveJobs($category_id = null)
    {
        $qb = $this-&gt;createQueryBuilder('j')
                   -&gt;where('j.expires_at &gt; :date')
                   -&gt;setParameter('date', date('Y-m-d H:i:s', time()))
                   -&gt;orderBy('j.expires_at', 'DESC');

        if ($category_id) {
            $qb-&gt;andWhere('j.category = :category_id')
               -&gt;setParameter('category_id', $category_id);
        }

        $query = $qb-&gt;getQuery();

        return $query-&gt;getResult();
    }
}</pre><p>现在<em>action</em>里面就可以使用刚才添加的<em>getActiveJobs()</em>方法了。</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

    public function indexAction()
    {
        $em = $this-&gt;getDoctrine()-&gt;getManager();

        $entities = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getActiveJobs();

        return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(
            'entities' =&gt; $entities
        ));
    }

// ...</pre><p>上面重构后的代码比起之前的未重构代码有如下优点：</p>
<ul class="task-list">
<li>获取有效期内的<em>Job</em>数据的代码现在位于<em>Model</em>层中</li>
<li><em>JobController::indexAction()</em>中的代码更少了，可读性也提高了</li>
<li><em>getActiveJobs()</em>方法可以被重用</li>
<li>更加容易地对<em>model</em>的代码进行测试</li>
</ul>
<h2><a id="user-content-首页中的categories" class="anchor" href="https://github.com/happen-zhang/symfony2-jobeet-tutorial/blob/master/chapter-06/chapter-06.md#%E9%A6%96%E9%A1%B5%E4%B8%AD%E7%9A%84categories"></a>首页中的<em>Categories</em></h2>
<p>根据我们第二天内容中的需求，<em>Job</em>信息能够按照分类进行显示。直到现在我们都还没考虑到对<em>Job</em>信息进行分类显示。按照第二天内容中的需求，我们需要在首页中按照不同的分类来显示<em>Job</em>信息。首先，我们需要获得包含有未过期的<em>Job</em>数据的所有分类。</p>
<p>为<em>Category</em>实体创建一个<em>Repository</em>类：</p><pre class="crayon-plain-tag"># src/Ibw/JobeetBundle/Resources/config/doctrine/Category.orm.yml
Ibw\JobeetBundle\Entity\Category:
    type: entity
    repositoryClass: Ibw\JobeetBundle\Repository\CategoryRepository
    #...</pre><p>生成<em>Repository</em>类：</p><pre class="crayon-plain-tag">php app/console doctrine:generate:entities IbwJobeetBundle</pre><p>打开<em>src/Ibw/JobeetBundle/Repository/CategoryRepository.php</em>文件，添加<em>getWithJobs()</em>方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Repository/CategoryRepository.php
namespace Ibw\JobeetBundle\Repository;

use Doctrine\ORM\EntityRepository;

/**
 * CategoryRepository
 *
 * This class was generated by the Doctrine ORM. Add your own custom
 * repository methods below.
 */
class CategoryRepository extends EntityRepository
{
    public function getWithJobs()
    {
        $query = $this-&gt;getEntityManager()-&gt;createQuery(
            'SELECT c FROM IbwJobeetBundle:Category c LEFT JOIN c.jobs j WHERE j.expires_at &gt; :date'
        )-&gt;setParameter('date', date('Y-m-d H:i:s', time()));

        return $query-&gt;getResult();
    }   
}</pre><p>同时我们需要修改<em>indexAction()</em>方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

    public function indexAction()
    {
        $em = $this-&gt;getDoctrine()-&gt;getManager();

        $categories = $em-&gt;getRepository('IbwJobeetBundle:Category')-&gt;getWithJobs();

        foreach($categories as $category) {
            $category-&gt;setActiveJobs($em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getActiveJobs($category-&gt;getId()));
        }

        return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(
            'categories' =&gt; $categories
        ));
    }

// ...</pre><p>我们在上面的代码中可以看到<em>Category</em>有个<em>setActiveJobs()</em>方法，那么现在我们来修改这个方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Entity/Category.php
class Category
{
    // ...

    private $active_jobs;

    // ...

    public function setActiveJobs($jobs)
    {
        $this-&gt;active_jobs = $jobs;
    }

    public function getActiveJobs()
    {
        return $this-&gt;active_jobs;
    }
}</pre><p>在模板中，我们需要通过迭代变量<em>categories</em>的值来显示所有的<em>Job</em>数据：</p><pre class="crayon-plain-tag">&lt;!-- src/Ibw/JobeetBundle/Resources/views/Job/index.html.twig --&gt;
&lt;!-- ... --&gt;
{% block content %}
    &lt;div id="jobs"&gt;
        {% for category in categories %}
            &lt;div&gt;
                &lt;div class="category"&gt;
                    &lt;div class="feed"&gt;
                        &lt;a href=""&gt;Feed&lt;/a&gt;
                    &lt;/div&gt;
                    &lt;h1&gt;{{ category.name }}&lt;/h1&gt;
                &lt;/div&gt;
                &lt;table class="jobs"&gt;
                    {% for entity in category.activejobs %}
                        &lt;tr class="{{ cycle(['even', 'odd'], loop.index) }}"&gt;
                            &lt;td class="location"&gt;{{ entity.location }}&lt;/td&gt;
                            &lt;td class="position"&gt;
                                &lt;a href="{{ path('ibw_job_show', { 'id': entity.id, 'company': entity.companyslug, 'location': entity.locationslug, 'position': entity.positionslug }) }}"&gt;
                                    {{ entity.position }}
                                &lt;/a&gt;
                            &lt;/td&gt;
                             &lt;td class="company"&gt;{{ entity.company }}&lt;/td&gt;
                        &lt;/tr&gt;
                    {% endfor %}
                &lt;/table&gt;
            &lt;/div&gt;
        {% endfor %}
    &lt;/div&gt;
{% endblock %}</pre><p>&nbsp;</p>
<h2>限制结果行数</h2>
<p>我们现在需要限制<em>Job</em>信息列表中显示的行数为10行。实现这个功能非常简单，我们来给<em>JobRepository::getActiveJobs()</em>方法添加一个<em>$max</em>参数：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Repository/JobRepository.php
public function getActiveJobs($category_id = null, $max = null)
{
    $qb = $this-&gt;createQueryBuilder('j')
        -&gt;where('j.expires_at &gt; :date')
        -&gt;setParameter('date', date('Y-m-d H:i:s', time()))
        -&gt;orderBy('j.expires_at', 'DESC');

    if($max) {
        $qb-&gt;setMaxResults($max);
    }

    if($category_id) {
        $qb-&gt;andWhere('j.category = :category_id')
            -&gt;setParameter('category_id', $category_id);
    }

    $query = $qb-&gt;getQuery();

    return $query-&gt;getResult();
}</pre><p>修改<em>indexAction()</em>方法中的代码，我们需要使用<em>$max</em>参数来调用<em>getActiveJobs()</em>方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

public function indexAction()
{
    $em = $this-&gt;getDoctrine()-&gt;getManager();

    $categories = $em-&gt;getRepository('IbwJobeetBundle:Category')-&gt;getWithJobs();

    foreach($categories as $category)
    {
        $category-&gt;setActiveJobs($em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getActiveJobs($category-&gt;getId(), 10));
    }

    return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(
        'categories' =&gt; $categories
    ));
}

// ...</pre><p>&nbsp;</p>
<h2>自定义配置</h2>
<p>在<em>JobController::indexAction()</em>方法中，我们对返回<em>Job</em>数据的行数（<em>$max = 10</em>）进行了<strong>硬编码（hardcode）</strong>，我们需要让返回行数的值是可配置的。在<em>Symfony</em>中，我们可以在<em>app/config/config.yml</em>文件中的<em>parameters</em>区块中自定义一些配置（如果<em>parameters</em>区块不存在的话，那么我们也可以自行创建它）。</p><pre class="crayon-plain-tag"># app/config/config.yml
# ...

parameters:
    max_jobs_on_homepage: 10</pre><p>定义好后我们就可以在<em>Controller</em>中访问它们的值了：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

    public function indexAction()
    {
        $em = $this-&gt;getDoctrine()-&gt;getManager();

        $categories = $em-&gt;getRepository('IbwJobeetBundle:Category')-&gt;getWithJobs();

        foreach($categories as $category) {
            $category-&gt;setActiveJobs($em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getActiveJobs($category-&gt;getId(), $this-&gt;container-&gt;getParameter('max_jobs_on_homepage')));
        }

        return $this-&gt;render('IbwJobeetBundle:Job:index.html.twig', array(
            'categories' =&gt; $categories
        ));
    }

// ...</pre><p>&nbsp;</p>
<h2>动态<em>Fixtures</em></h2>
<p>对于上面所做的修改，我们还不能在页面中看到有什么变化，因为现在我们的数据库中只有很少量的<em>Job</em>数据。现在我们需要在<em>Fixture</em>中批量添加<em>Job</em>数据。我们可选择手动复制之前已存在的代码来重复进行生成<em>Job</em>数据，但我们有更好的办法。我们需要引起注意的是，重复的代码给人的感觉就是很差，甚至在<em>Fixture</em>文件中出现重复的代码：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/DataFixtures/ORM/LoadJobData.php
// ...

public function load(ObjectManager $em)
{
    // ...

    for($i = 100; $i &lt;= 130; $i++)
    {
        $job = new Job();
        $job-&gt;setCategory($em-&gt;merge($this-&gt;getReference('category-programming')));
        $job-&gt;setType('full-time');
        $job-&gt;setCompany('Company '.$i);
        $job-&gt;setPosition('Web Developer');
        $job-&gt;setLocation('Paris, France');
        $job-&gt;setDescription('Lorem ipsum dolor sit amet, consectetur adipisicing elit.');
        $job-&gt;setHowToApply('Send your resume to lorem.ipsum [at] dolor.sit');
        $job-&gt;setIsPublic(true);
        $job-&gt;setIsActivated(true);
        $job-&gt;setToken('job_'.$i);
        $job-&gt;setEmail('job@example.com');

        $em-&gt;persist($job);
    }

    // ... 
    $em-&gt;flush();
}

// ...</pre><p>现在用<code>doctrine:fixtures:load</code>命令来重新加载<em>Fixture</em>，观察<em>Programming</em>分类下的<em>Job</em>信息行数是否为10行：</p>
<p><img class="alignnone size-large wp-image-349" src="http://www.newlifeclan.com/symfony/wp-content/uploads/sites/2/2015/03/06-02-1024x556.png" alt="06-02" width="780" height="423" /></p>
<h2>过期的<em>Job</em>页面</h2>
<p>如果一个<em>Job</em>不在有效期限内了，那么它将是不再可能被用户访问到的，即使用户知道它所在的<em>URL</em>也不行。我们可以尝试着访问过期的<em>Job</em>信息的页面（获得过期的<em>Job</em>信息的<em>id</em>的方法：<code>select id, token from job where expires_at &lt; NOW()</code>，然后把获得的id替换下面<em>URL</em>中<em>ID</em>的值）:</p><pre class="crayon-plain-tag">/app_dev.php/job/sensio-labs/paris-france/ID/web-developer-expired</pre><p>每当用户访问过期的<em>Job</em>信息页面时，我们应该让用户重定向到<strong>404页面</strong>。现在我们来给<em>JobRepository</em>添加一个方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Repository/JobRepository.php
// ...

public function getActiveJob($id)
{
    $query = $this-&gt;createQueryBuilder('j')
        -&gt;where('j.id = :id')
        -&gt;setParameter('id', $id)
        -&gt;andWhere('j.expires_at &gt; :date')
        -&gt;setParameter('date', date('Y-m-d H:i:s', time()))
        -&gt;setMaxResults(1)
        -&gt;getQuery();

    try {
        $job = $query-&gt;getSingleResult();
    } catch (\Doctrine\Orm\NoResultException $e) {
        $job = null;
    }

    return $job;
}</pre><p></p>
<blockquote><p> 如果没有结果被返回，那么<em>getSingleResult()</em>方法会抛出Doctrine\ORM\NoResultException异常；如果返回的结果不止一个，那么<em>getSingleResult()</em>方法会抛出Doctrine\ORM\NonUniqueResultException异常。如果你使用<em>getSingleResult()</em>方法，那么请用<em>try&#8230;catch</em>语句包含它，以至于确保所返回的结果集只有一行数据。</p></blockquote>
<p>现在修改<em>JobController::showAction()</em>方法：</p><pre class="crayon-plain-tag">// src/Ibw/JobeetBundle/Controller/JobController.php
// ...

$entity = $em-&gt;getRepository('IbwJobeetBundle:Job')-&gt;getActiveJob($id);

// ...</pre><p>现在我们去访问一个已过期的<em>Job</em>信息页面，我们会被重定向到<strong>404页面</strong>：</p>
<p><img class="alignnone size-large wp-image-350" src="http://www.newlifeclan.com/symfony/wp-content/uploads/sites/2/2015/03/06-03-1024x557.png" alt="06-03" width="780" height="424" /></p>
<p>好了，我们今天就到这吧。我们明天会开始实现<em>Category</em>页面。</p>
<p>&nbsp;</p>
<p><span style="color: #ff0000">原文链接：<a style="color: #ff0000" href="http://www.intelligentbee.com/blog/2013/08/12/symfony2-jobeet-day-6-more-with-the-model/">http://www.intelligentbee.com/blog/2013/08/12/symfony2-jobeet-day-6-more-with-the-model/</a></span></p>
<p>&nbsp;</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/347">jobeet第六天:更多的数据模型</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/347/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>第十三章：安全Security</title>
		<link>http://www.newlifeclan.com/symfony/archives/215</link>
		<comments>http://www.newlifeclan.com/symfony/archives/215#comments</comments>
		<pubDate>Mon, 15 Dec 2014 08:51:59 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[其他]]></category>
		<category><![CDATA[symfony2.6]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=215</guid>
		<description><![CDATA[<p>Security(安全)的目的是防止用户访问他们不应该访问的资源，需要两个步骤实现这个过程。 该方法的第一个步 [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/215">第十三章：安全Security</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>Security(安全)的目的是防止用户访问他们不应该访问的资源，需要两个步骤实现这个过程。<br />
<span id="more-215"></span></p>
<p>该方法的第一个步骤中，知道该安全系统识别用户是谁，要求用户提交某种身份识别。这就是所谓的authentication（认证），这意味着该系统试图找出你是谁。<br />
一旦系统知道了你是谁，进入下一个步骤来确定是否能够访问指定的资源。这个过程被称为authorization（授权），它的意思是系统正在检查，看看是否有权限来执行某一动作。<br />
<img class="alignnone size-full wp-image-219" src="http://www.newlifeclan.com/symfony/wp-content/uploads/sites/2/2014/12/security_authentication_authorization.png" alt="security_authentication_authorization" width="491" height="282" /><br />
最好的学习方法是看一个例子，假设你想在您的应用程序中使用HTTP基本身份验证。</p>
<blockquote><p><span style="text-decoration: underline"><span style="color: #ff0000"><a style="color: #ff0000;text-decoration: underline" href="http://symfony.com/doc/current/components/security/introduction.html" target="_blank">Symfony2的安全组件</a></span></span>作为一个独立的PHP库可以用于任何PHP项目中。</p></blockquote>
<h2>基本的例子：HTTP Authentication（认证）</h2>
<p>安全组件可以通过应用程序配置进行配置。实际上，最标准的安全设置只是使用了正常的配置。<br />
下面的配置是告诉symfony，对于任何匹配 /admin/* 的URL都需要保护，并要求使用基本的HTTP Authentication（认证）（即老派的用户名／密码对话框）来询问用户凭证。</p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    firewalls:
        secured_area:
            pattern:   ^/
            anonymous: ~
            http_basic:
                realm: "Secured Demo Area"

    access_control:
        - { path: ^/admin/, roles: ROLE_ADMIN }
        # Include the following line to also secure the /admin path itself
        # - { path: ^/admin$, roles: ROLE_ADMIN }

    providers:
        in_memory:
            memory:
                users:
                    ryan:  { password: ryanpass, roles: 'ROLE_USER' }
                    admin: { password: kitten, roles: 'ROLE_ADMIN' }

    encoders:
        Symfony\Component\Security\Core\User\User: plaintext</pre><p></p>
<blockquote><p> 一个标准的Symfony2发布将安全配置单独放入单个文件（如：app/config/security.yml）。如果你没有单独的安全文件，你可以直接将配置放入你的主配置文件中（如app/config/config.yml）。</p></blockquote>
<p>该配置最终结果是一个全功能的安全系统，如下所示：</p>
<ul>
<li>有两个用户在系统中（ryan和admin）；</li>
<li>用户可以通过基本的HTTP身份验证提示来验证自己；</li>
<li>任何匹配/admin/*的URL都会经过安全认证，只有admin用户可以访问它；</li>
<li>所有不匹配/admin/*的URL可以被所有用户访问（永远不会提示用户登录）；</li>
</ul>
<p>让我们简要地看看安全是如何工作的，以及配置的每一部分是如何在一起工作的。</p>
<h3>Security怎样工作：认证和授权</h3>
<p>Symfony2安全系统的工作就是确定用户是谁（认证）然后检查看看用户是否有访问特定资源或URL的权限。</p>
<h3>防火墙（认证）</h3>
<p>当一个用户将一个请求发向一个被防火墙保护的URL时，安全系统就被激活了。防火墙的工作就是要确保用户是不是需要被认证，如果需要的话，发送一个响应返回给用户去启动认证过程。<br />
当传入请求的URL匹配防火墙的正则表达式pattern(模式)时，防火墙被激活。在本例中，pattern（模式） (^/)将匹配任何传入的请求。然而，其实防火墙被激活也没有意义，任何URL都不会使HTTP认证用户名和密码对话框出现。例如，任何用户都可以访问/foo，而不会提示要去认证。</p>
<blockquote><p>您也可以匹配的请求的其他信息（如主机和方法）的请求。欲了解更多信息和示例，阅读 <a href="http://symfony.com/doc/current/cookbook/security/firewall_restriction.html" target="_blank">如<span style="text-decoration: underline"><span style="color: #ff0000;text-decoration: underline">何限制防火墙到一个特定的请求</span></span></a>。</p></blockquote>
<p><img class="alignnone size-full wp-image-220" src="http://www.newlifeclan.com/symfony/wp-content/uploads/sites/2/2014/12/security_anonymous_user_access.png" alt="security_anonymous_user_access" width="542" height="383" /><br />
之所以这样首先是因为通过anonymous（匿名）配置参数，防火墙允许匿名用户通过。换句话说，防火墙不要求用户立刻进行认证。其次是因为没有特定的role（角色）需要访问/foo（下面的access_control部分），请求甚至可以在没有要求用户认证的情况下完成。<br />
如果你删除了anonymous（匿名）关键词，那么防火墙总是要求用户立即认证的。</p>
<h2>访问控制（授权）</h2>
<p>然而如果用户请求/admin/foo，那么处理过程是不同的。这是因为access_control配置部指出任何匹配正则表达式^/admin（如/admin或任何匹配/admin/*的URL）都需要ROLE_ADMIN角色。角色是大多数授权的基础：如果用户拥有ROLE_ADMIN角色，用户可以访问/admin/foo。<br />
<img class="alignnone size-full wp-image-222" src="http://www.newlifeclan.com/symfony/wp-content/uploads/sites/2/2014/12/security_anonymous_user_denied_authorization.png" alt="security_anonymous_user_denied_authorization" width="526" height="428" /><br />
像前面一样，当用户最初发送请求时，防火墙并不要求任何身份。然而当访问控制层拒绝用户访问（因为匿名用户没有ROLE_ADMIN用户）时，防火墙接管该过程，并启动认证进程。认证依赖你所用的认证机制。例如，如果你使用表单登录认证方式，用户将被重定向到登录页面。如果你使用的是HTTP认证，用户将被发送HTTP的401响应，以便用户可以看到用户名密码对话框。<br />
用户现在有机会提交它的证书给应用程序。如果证书是有效的，那么原始请求会被重发。<br />
<img class="alignnone size-full wp-image-223" src="http://www.newlifeclan.com/symfony/wp-content/uploads/sites/2/2014/12/security_ryan_no_role_admin_access.png" alt="security_ryan_no_role_admin_access" width="542" height="436" /><br />
在本例中，用户ryan成功通过防火墙认证。但因为ryan没有拥有ROLE_ADMIN角色，它仍然被拒绝访问/admin/foo。最终这意味着用户将看到消息说明访问被拒绝。<br />
当Symfony2拒绝用户访问时，用户可以看到一个错误页面并收到一个HTTP的403状态码（Forbidden）。你可以根据食谱（cookbook）<span style="text-decoration: underline"><span style="color: #ff0000"><a style="color: #ff0000;text-decoration: underline" href="http://symfony.com/doc/current/cookbook/controller/error_pages.html#cookbook-error-pages-by-status-code" target="_blank">错误页面</a></span></span>中的自定义403错误页内容来自定义拒绝访问页。</p>
<p>最后，如果admin用户请求/admin/foo，相拟的进程会发生。但是在认证之后，访问控制层将让请求通过：</p>
<p><img class="alignnone size-full wp-image-224" src="http://www.newlifeclan.com/symfony/wp-content/uploads/sites/2/2014/12/090950434.png" alt="090950434" width="561" height="405" /></p>
<p>当用户请求一个受保护资源时产生的请求流是十分简单的，但相当灵活。正如你稍后将看到的那样，认证可以通过多样方式来处理，包括通过表单登录、X.509证书或通过Twitter来认证用户。无论认证的模式如何，请求流总是相同的：</p>
<ol>
<li>用户访问受限资源；</li>
<li>应用程序将用户重定向给登录表单；</li>
<li>用户提交它的证书（如：用户名／密码）；</li>
<li>防火墙认证用户；</li>
<li>认证用户重发原始请求。</li>
</ol>
<blockquote><p>确切的过程实际取决于你所使用的认证机制。举个例子，当使用表单登录时，用户提交它的证书到一个处理表单的URL（如 /login_check），然后重定向到最初请求的URL（如 /admin/foo）。但是如果是HTTP认证，用户将直接提供它的证书到原始URL（如 /admin/foo），然后在同一请求中将页面返回给用户（即：不进行重定向）<br />
这些特质不会引起任何问题，但记住它们是有好处的。</p></blockquote>
<blockquote><p>你也将在稍后学到在Symfony2中如何保证其它事物的安全，包括特定的控制器、对象、甚至是PHP方法。</p></blockquote>
<h2>使用传统的登录表单</h2>
<blockquote><p>在本节，security.yml文件中，继续使用硬编码定义用户，然后你需要学习怎样去创建基本的登录表单。<br />
如果从数据库中加载用户，你需要阅读<span style="text-decoration: underline"><span style="color: #ff0000"><a style="color: #ff0000;text-decoration: underline" href="http://symfony.com/doc/current/cookbook/security/entity_provider.html" target="_blank">How to Load Security Users from the Database (the Entity Provider)</a></span></span>。<br />
通过阅读这篇文章的这一节,您可以从数据库中加载用户创建一个完整的登录表单系统。</p></blockquote>
<p>在目前为止，你已经看到如何将你的应用程序放置在防火墙下，然后根据规则限制访问某些区域。通过使用HTTP认证，你可以毫不费力地进入所有浏览器都可以提供的用户名／密码框。然而，Symfony2支持许多对话框以外的认证机制。所有这一切的细节，可参见<span style="text-decoration: underline"><span style="color: #ff0000"><a style="color: #ff0000;text-decoration: underline" href="http://symfony.com/doc/current/reference/configuration/security.html" target="_blank">安全配置参考</a></span></span>。<br />
在本节中，你将让用户通过一个传统的HTML登录表单来增强这一过程。<br />
首先，在你的防火墙下启动表单登录：</p><pre class="crayon-plain-tag"># app/config/security.yml
security:
    firewalls:
        secured_area:
            pattern:   ^/
            anonymous: ~
            form_login:
                login_path: login
                check_path: login_check</pre><p></p>
<blockquote><p> 如果你不需要自定义你的login_path或check_path的值（这里的值是缺省值），你可以缩写你的配置：</p><pre class="crayon-plain-tag">form_login: ~</pre><p>&nbsp;</p></blockquote>
<p>现在，当安全系统启动认证过程时，它将用户重定向到登录表单（默认情况下是/login）。实现这个登录表单。首先创建两个路由：一个login路由显示登录表单（如：/login），一个login_check将处理登录表单的提交（如：/login_check）：</p><pre class="crayon-plain-tag"># app/config/routing.yml
login:
    path:     /login
    defaults: { _controller: AcmeSecurityBundle:Security:login }
login_check:
    path: /login_check</pre><p></p>
<blockquote><p> 你不需要为/login_check的URL实现控制器，因为防火墙会自动捕捉和处理这一URL上的任何表单提交。然而，你一定要有个路由（如下所示）URL，以及一个用于注销的路径（查看本页 Logging Out）。</p></blockquote>
<p>注意，login路由匹配login_path的配置值，因为在那里安全系统将进行重定向需要登录的用户。</p>
<p>下一步，创建显示登录表单的控制器：</p><pre class="crayon-plain-tag">// src/Acme/SecurityBundle/Controller/SecurityController.php;
namespace Acme\SecurityBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Security;

class SecurityController extends Controller
{
    public function loginAction(Request $request)
    {
        $session = $request-&gt;getSession();

        // get the login error if there is one
        if ($request-&gt;attributes-&gt;has(Security::AUTHENTICATION_ERROR)) {
            $error = $request-&gt;attributes-&gt;get(
                Security::AUTHENTICATION_ERROR
            );
        } elseif (null !== $session &amp;&amp; $session-&gt;has(Security::AUTHENTICATION_ERROR)) {
            $error = $session-&gt;get(Security::AUTHENTICATION_ERROR);
            $session-&gt;remove(Security::AUTHENTICATION_ERROR);
        } else {
            $error = '';
        }

        // last username entered by the user
        $lastUsername = (null === $session) ? '' : $session-&gt;get(Security::LAST_USERNAME);

        return $this-&gt;render(
            'AcmeSecurityBundle:Security:login.html.twig',
            array(
                // last username entered by the user
                'last_username' =&gt; $lastUsername,
                'error'         =&gt; $error,
            )
        );
    }
}</pre><p>不要让这个控制器迷惑你。正如你将稍后看到的那样，当用户提交表单时，安全系统自动为你处理表单提交。如果用户提交了一个非法的用户名或密码，控制器会从安全系统中读到表单提交的错误，以便返回给用户显示。</p>
<p>换句话说，你的工作是显示登录表单以及可能发生的错误，但安全系统自身检查提交的用户名和密码，并对该用户进行认证。</p>
<p>最后，创建相应的表单：</p><pre class="crayon-plain-tag">{# src/Acme/SecurityBundle/Resources/views/Security/login.html.twig #}
{% if error %}
    &lt;div&gt;{{ error.message }}&lt;/div&gt;
{% endif %}

&lt;form action="{{ path('login_check') }}" method="post"&gt;
    &lt;label for="username"&gt;Username:&lt;/label&gt;
    &lt;input type="text" id="username" name="_username" value="{{ last_username }}" /&gt;

    &lt;label for="password"&gt;Password:&lt;/label&gt;
    &lt;input type="password" id="password" name="_password" /&gt;

    {#
        If you want to control the URL the user
        is redirected to on success (more details below)
        &lt;input type="hidden" name="_target_path" value="/account" /&gt;
    #}

    &lt;button type="submit"&gt;login&lt;/button&gt;
&lt;/form&gt;</pre><p></p>
<blockquote><p> 此登录表单目前无法防止CSRF攻击，阅读 <span style="text-decoration: underline;color: #ff0000"><a style="color: #ff0000;text-decoration: underline" href="http://symfony.com/doc/current/cookbook/security/csrf_in_login_form.html" target="_blank">在登录窗体使用CSRF保护</a></span>如何保护您的登录表单</p>
<p>被送入模板的错误变量是<span style="text-decoration: underline"><span style="color: #ff0000"><a style="color: #ff0000;text-decoration: underline" href="http://api.symfony.com/2.6/Symfony/Component/Security/Core/Exception/AuthenticationException.html" target="_blank">AuthenticationException</a></span></span>的实例。它也许包含更多关于认证失败的信息，甚至是敏感信息。所以它使用的非常广泛！</p></blockquote>
<p>表单有着非常少的要求。首先，通过提交表单到/login_check（通过login_check路由），安全系统自动为你截取表单提交并进行表单处理。其次，安全系统预期被提交的表单项是_username和_password（这些表单项名可以被<span style="text-decoration: underline;color: #ff0000"><a style="color: #ff0000;text-decoration: underline" href="http://symfony.com/doc/current/reference/configuration/security.html#reference-security-firewall-form-login" target="_blank">配置</a></span>）。</p>
<p>这是这样！当你提交表单时，安全系统将自动检查用户的证书，要么认证通过用户，要么将用户重定向到登录表单以显示错误信息。</p>
<p>回顾整个过程：</p>
<ol>
<li>用户尝试访问受限用户；</li>
<li>防火墙通过将用户重定向到登录表单（/login）并启动认证过程；</li>
<li>在本例中，/login页通过创建路由和控制器来渲染登录页面；</li>
<li>用户提交登录表单到/login_check；</li>
<li>安全系统拦截请求，检查用户提交的证书，如果正确的话就认证通过用户，反之则将用户送回登录页面。</li>
</ol>
<p>默认情况下，如果提交的证书是正确的，用户将被重定向到被请求的原始页（如：/admin/foo）。如果用户本来就直接访问的login页面，它将被重定向到主页。这可以允许你去定制，例如，重定向用户到指定的URL。<br />
关于这个的更多细节，以及如何自定义表单登录过程，请参见如何自定义你的<a href="http://symfony.com/doc/current/cookbook/security/form_login.html" target="_blank">表单登录</a>。</p>
<p><strong>避免常见错误</strong><br />
在设置登录表单时，注意一些常见的陷阱。<br />
1. 创建正确的路由<br />
首先，确保你已经正确地定义了/login和/login_check路由，它们对应着login_path和check_path配置值。这里的一个错误配置可能会将你重定向到404页，而非登录页面，或者提交登录表单之后什么事情也没发生（你只是一遍又一遍地看到登录页面）。<br />
2. 确保登录页面是不安全的<br />
同样，也需要确保登录页面是不要求任何角色就可以查看的。例如，下面的配置，为所有的URL要求ROLE_ADMIN角色（包括/login的URL），会引起循环重定义的：</p><pre class="crayon-plain-tag"># app/config/security.yml

# ...
access_control:
    - { path: ^/, roles: ROLE_ADMIN }</pre><p>在/login的URL上删除访问控制以修复该问题：</p><pre class="crayon-plain-tag"># app/config/security.yml

# ...
access_control:
    - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - { path: ^/, roles: ROLE_ADMIN }</pre><p>同样，如果你的防火墙不允许匿名用户的话，你也需要创建一个特殊的防火墙来让匿名用户使用登录页：</p><pre class="crayon-plain-tag"># app/config/security.yml

# ...
firewalls:
    login_firewall:
        pattern:   ^/login$
        anonymous: ~
    secured_area:
        pattern:    ^/
        form_login: ~</pre><p>3. 确保“/login_check”在防火墙之后</p>
<p>接下来，确保你check_path的URL（如：/login_check）在你登录表单的防火墙之后（在本例中，单个防火墙匹配所有URL，包含/login_check）。如果/login_check没有匹配任何防火墙，你将得到不能为路径“/login_check&#8221;找到控制器的异常。</p>
<p>4. 多个防火墙不能共享安全内容</p>
<p>如果你使用多重防火墙，并且你在针对一个防火墙进行认证，那么你将不会再自动针对其它的防火墙进行认证。不同的防火墙就象不同的安全系统。这也是为什么对于大多数应用程序而言，有一个主要的防火墙就足够了的原因。</p>
<p>5.路由错误页面不包含在防火墙里</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>未完待续中&#8230;..</p>
<p>&nbsp;</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/215">第十三章：安全Security</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/215/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>(Doctrine)如何处理Doctrine文件上传</title>
		<link>http://www.newlifeclan.com/symfony/archives/199</link>
		<comments>http://www.newlifeclan.com/symfony/archives/199#comments</comments>
		<pubDate>Thu, 04 Dec 2014 06:36:11 +0000</pubDate>
		<dc:creator><![CDATA[napoleon]]></dc:creator>
				<category><![CDATA[Cook 2.6]]></category>
		<category><![CDATA[Cookbook]]></category>
		<category><![CDATA[Doctrine]]></category>
		<category><![CDATA[symfony2.6]]></category>

		<guid isPermaLink="false">http://www.newlifeclan.com/symfony/?p=199</guid>
		<description><![CDATA[<p>以Doctrine形式上传文件和一些其他方式上传图片是没有什么不同的。换句话说，你可以在表单提交后，自由的处理 [&#8230;]</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/199">(Doctrine)如何处理Doctrine文件上传</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></description>
				<content:encoded><![CDATA[<p>以Doctrine形式上传文件和一些其他方式上传图片是没有什么不同的。换句话说，你可以在表单提交后，自由的处理文件。对于如何做到这一点，请参阅 <a href="http://symfony.com/doc/current/reference/forms/types/file.html" target="_blank">http://symfony.com/doc/current/reference/forms/types/file.html</a></p>
<p><span id="more-199"></span></p>
<p>你可以将上传文件的处理集成到Entity的生命周期中（如创建，修改和删除时调用）。</p>
<p>在这种情况下的好处，在Doctrine中创建、修改和删除数据时，上传文件和删除文件也会自动完成（必须在控制器中做任何操作）</p>
<h2>基本设置</h2>
<p>创建一个简单的Doctrine实体类：</p><pre class="crayon-plain-tag">// src/Acme/DemoBundle/Entity/Document.php
namespace Acme\DemoBundle\Entity;

use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Validator\Constraints as Assert;

/**
 * @ORM\Entity
 */
class Document
{
    /**
     * @ORM\Id
     * @ORM\Column(type="integer")
     * @ORM\GeneratedValue(strategy="AUTO")
     */
    public $id;

    /**
     * @ORM\Column(type="string", length=255)
     * @Assert\NotBlank
     */
    public $name;

    /**
     * @ORM\Column(type="string", length=255, nullable=true)
     */
    public $path;

    public function getAbsolutePath()
    {
        return null === $this-&gt;path
            ? null
            : $this-&gt;getUploadRootDir().'/'.$this-&gt;path;
    }

    public function getWebPath()
    {
        return null === $this-&gt;path
            ? null
            : $this-&gt;getUploadDir().'/'.$this-&gt;path;
    }

    protected function getUploadRootDir()
    {
        // the absolute directory path where uploaded
        // documents should be saved
        return __DIR__.'/../../../../web/'.$this-&gt;getUploadDir();
    }

    protected function getUploadDir()
    {
        // get rid of the __DIR__ so it doesn't screw up
        // when displaying uploaded doc/image in the view.
        return 'uploads/documents';
    }
}</pre><p>该document实体有一个名称与文件相关联。这个path属性存储一个文件的相对路径并且在数据库中存储。这个getAbsolutePath()会返回一个绝对路径，getWebPath()会返回一个web路径，用于模板加入上传文件链接。</p>
<blockquote><p>如果你还没有这样做的话，你应该阅读http://symfony.com/doc/current/reference/forms/types/file.html首先了解基本的上传过程。</p>
<p>如果您使用注释来验证规则（如本例所示），请确保你启用了注释验证（见http://symfony.com/doc/current/book/validation.html#book-validation-configuration）。</p>
<p>&nbsp;</p></blockquote>
<p>在处理一个实际的文件上传时，使用一个“虚拟”的file字段。例如，如果你在controller中直接构建一个form，他可能是这样的：</p><pre class="crayon-plain-tag">public function uploadAction()
{
    // ...

    $form = $this-&gt;createFormBuilder($document)
        -&gt;add('name')
        -&gt;add('file')
        -&gt;getForm();

    // ...
}</pre><p>下一步，创建file这个属性到你的Document类中并且添加一些验证规则：</p><pre class="crayon-plain-tag">use Symfony\Component\HttpFoundation\File\UploadedFile;

// ...
class Document
{
    /**
     * @Assert\File(maxSize="6000000")
     */
    private $file;

    /**
     * Sets file.
     *
     * @param UploadedFile $file
     */
    public function setFile(UploadedFile $file = null)
    {
        $this-&gt;file = $file;
    }

    /**
     * Get file.
     *
     * @return UploadedFile
     */
    public function getFile()
    {
        return $this-&gt;file;
    }
}</pre><p>annotations</p><pre class="crayon-plain-tag">// src/Acme/DemoBundle/Entity/Document.php
namespace Acme\DemoBundle\Entity;

// ...
use Symfony\Component\Validator\Constraints as Assert;

class Document
{
    /**
     * @Assert\File(maxSize="6000000")
     */
    private $file;

    // ...
}</pre><p></p>
<blockquote><p> 当你使用File约束，symfony会自动猜测表单字段输入的是一个文件上传。这就是当你创建表单（-&gt;add(&#8216;file&#8217;)）时，为什么没有在表单明确设置为文件上传的原因。</p></blockquote>
<p>下面的控制器，告诉您如何处理全部过程：</p><pre class="crayon-plain-tag">// ...
use Acme\DemoBundle\Entity\Document;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;
// ...

/**
 * @Template()
 */
public function uploadAction(Request $request)
{
    $document = new Document();
    $form = $this-&gt;createFormBuilder($document)
        -&gt;add('name')
        -&gt;add('file')
        -&gt;getForm();

    $form-&gt;handleRequest($request);

    if ($form-&gt;isValid()) {
        $em = $this-&gt;getDoctrine()-&gt;getManager();

        $em-&gt;persist($document);
        $em-&gt;flush();

        return $this-&gt;redirect($this-&gt;generateUrl(...));
    }

    return array('form' =&gt; $form-&gt;createView());
}</pre><p>以前的controller当提交name自动的存储Document实体，但是他不会做任何关于文件的事情并且path属性也将是空白。</p>
<p>处理文件上传一个简单的方法就是在entity持久化之前设置相应的path属性。在某一时刻处理文件上传时，要调用Document实体类一个upload()方法给path赋值。</p><pre class="crayon-plain-tag">if ($form-&gt;isValid()) {
    $em = $this-&gt;getDoctrine()-&gt;getManager();

    $document-&gt;upload();

    $em-&gt;persist($document);
    $em-&gt;flush();

    return $this-&gt;redirect(...);
}</pre><p>这个upload()方法利用UploadedFile对象，是它提交后返回file字段：</p><pre class="crayon-plain-tag">public function upload()
{
    // the file property can be empty if the field is not required
    // 该file属性为空这个属性就不需要了
    if (null === $this-&gt;getFile()) {
        return;
    }

    // use the original file name here but you should
    // sanitize it at least to avoid any security issues
    //  这里你应该使用原文件名但是应该至少审核它避免一些安全问题
    // move takes the target directory and then the
    // target filename to move to
    // 将目标文件移动到目标目录
    $this-&gt;getFile()-&gt;move(
        $this-&gt;getUploadRootDir(),
        $this-&gt;getFile()-&gt;getClientOriginalName()
    );

    // set the path property to the filename where you've saved the file
    // 设置path属性为你保存文件的文件名
    $this-&gt;path = $this-&gt;getFile()-&gt;getClientOriginalName();

    // clean up the file property as you won't need it anymore
    // 清理你不需要的file属性
    $this-&gt;file = null;
}</pre><p></p>
<h2> 使用生命周期回调</h2>
<blockquote><p>生命周期回调是一种有限的技术，他有一些缺点。如果你想移除Document::getUploadRootDir()方法里的写死的编码__DIR__，最好的方法是开始使用Doctrine listeners。在哪里你将能够注入内核参数，如kernel.root_dir来建立绝对路径。</p></blockquote>
<p>这种原理工作，他有一个缺陷：也就是说当entity持久化时会有什么问题呢？答：该文件已经转移到了它的最终位置，实体类下的path属性不能够正确的实体化。</p>
<p>（如果entity有持久化问题或者文件不能够移动，什么事情也没有发生）为了避免这些问题，你应该改变这种实现方式以便数据库操作和自动删除文件：</p><pre class="crayon-plain-tag">/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Document
{
}</pre><p>接下来,利用这些回调函数重构<tt class="docutils literal"><code>Document</code></tt>类:</p><pre class="crayon-plain-tag">use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Document
{
    private $temp;

    /**
     * Sets file.
     *
     * @param UploadedFile $file
     */
    public function setFile(UploadedFile $file = null)
    {
        $this-&gt;file = $file;
        // check if we have an old image path
        // 检查如果我们有一个旧的图片路径
        if (isset($this-&gt;path)) {
            // store the old name to delete after the update
            $this-&gt;temp = $this-&gt;path;
            $this-&gt;path = null;
        } else {
            $this-&gt;path = 'initial';
        }
    }

    /**
     * @ORM\PrePersist()
     * @ORM\PreUpdate()
     */
    public function preUpload()
    {
        if (null !== $this-&gt;getFile()) {
            // do whatever you want to generate a unique name
            // 去生成一个唯一的名称
            $filename = sha1(uniqid(mt_rand(), true));
            $this-&gt;path = $filename.'.'.$this-&gt;getFile()-&gt;guessExtension();
        }
    }

    /**
     * @ORM\PostPersist()
     * @ORM\PostUpdate()
     */
    public function upload()
    {
        if (null === $this-&gt;getFile()) {
            return;
        }

        // if there is an error when moving the file, an exception will
        // be automatically thrown by move(). This will properly prevent
        // the entity from being persisted to the database on error
        //当移动文件发生错误，一个异常move（）会自动抛出异常。
        //这将阻止实体持久化数据库发生错误。
        $this-&gt;getFile()-&gt;move($this-&gt;getUploadRootDir(), $this-&gt;path);

        // check if we have an old image
        if (isset($this-&gt;temp)) {
            // delete the old image
            unlink($this-&gt;getUploadRootDir().'/'.$this-&gt;temp);
            // clear the temp image path
            $this-&gt;temp = null;
        }
        $this-&gt;file = null;
    }

    /**
     * @ORM\PostRemove()
     */
    public function removeUpload()
    {
        $file = $this-&gt;getAbsolutePath();
        if ($file) {
            unlink($file);
        }
    }
}</pre><p></p>
<blockquote><p> 如果更改你的entity是由Doctrine event listener 或event subscriber处理，这个 <tt class="docutils literal"><code>preUpdate()回调函数必须通知Doctrine关于正在做的改变。有关preUpdate事件限制的完整参考请查看 http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/events.html#preupdate</code></tt></p></blockquote>
<p>现在这个类做了你需要的一切：他会在entity持久化之前生成一个唯一的文件名，持久化之后，移动文件，删除文件。</p>
<p>现在移动文件是entity自动完成，这个$document-&gt;upload()就应该从controller中移除了：</p><pre class="crayon-plain-tag">if ($form-&gt;isValid()) {
    $em = $this-&gt;getDoctrine()-&gt;getManager();

    $em-&gt;persist($document);
    $em-&gt;flush();

    return $this-&gt;redirect(...);
}</pre><p></p>
<blockquote><p> 这个@ORM\PrePersist()和@ORM\PostPersist()事件回调:一个是在entity持久化到数据库之前触发,一个是在entity持久化到数据库之后触发。另一方面， <tt class="docutils literal"><code>@ORM\PreUpdate()</code></tt> 和 <tt class="docutils literal"><code>@ORM\PostUpdate()事件回调时当实体更新时触发。</code></tt></p>
<p>当改变entity字段后进行持久化操作时，<tt class="docutils literal"><code>PreUpdate</code></tt>和<tt class="docutils literal"><code>PostUpdate回调才会被触发。这意味着，默认情况下，你只改变了$file属性，这些事件不会被触发，因为这个属性它自己不会持久化到Doctrine。有一个解决方法，就是创建一个updated字段把它持久化到Doctrine，并当文件改变时手动调整它。</code></tt></p></blockquote>
<h2></h2>
<h2>使用ID作为文件名</h2>
<p>如果要使用ID作为文件名，实现略有不同，您需要保存path属性为文件扩展名,而不是实际的文件名:</p><pre class="crayon-plain-tag">use Symfony\Component\HttpFoundation\File\UploadedFile;

/**
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Document
{
    private $temp;

    /**
     * Sets file.
     *
     * @param UploadedFile $file
     */
    public function setFile(UploadedFile $file = null)
    {
        $this-&gt;file = $file;
        // check if we have an old image path
        if (is_file($this-&gt;getAbsolutePath())) {
            // store the old name to delete after the update
            $this-&gt;temp = $this-&gt;getAbsolutePath();
        } else {
            $this-&gt;path = 'initial';
        }
    }

    /**
     * @ORM\PrePersist()
     * @ORM\PreUpdate()
     */
    public function preUpload()
    {
        if (null !== $this-&gt;getFile()) {
            $this-&gt;path = $this-&gt;getFile()-&gt;guessExtension();
        }
    }

    /**
     * @ORM\PostPersist()
     * @ORM\PostUpdate()
     */
    public function upload()
    {
        if (null === $this-&gt;getFile()) {
            return;
        }

        // check if we have an old image
        if (isset($this-&gt;temp)) {
            // delete the old image
            unlink($this-&gt;temp);
            // clear the temp image path
            $this-&gt;temp = null;
        }

        // you must throw an exception here if the file cannot be moved
        // so that the entity is not persisted to the database
        // which the UploadedFile move() method does
        $this-&gt;getFile()-&gt;move(
            $this-&gt;getUploadRootDir(),
            $this-&gt;id.'.'.$this-&gt;getFile()-&gt;guessExtension()
        );

        $this-&gt;setFile(null);
    }

    /**
     * @ORM\PreRemove()
     */
    public function storeFilenameForRemove()
    {
        $this-&gt;temp = $this-&gt;getAbsolutePath();
    }

    /**
     * @ORM\PostRemove()
     */
    public function removeUpload()
    {
        if (isset($this-&gt;temp)) {
            unlink($this-&gt;temp);
        }
    }

    public function getAbsolutePath()
    {
        return null === $this-&gt;path
            ? null
            : $this-&gt;getUploadRootDir().'/'.$this-&gt;id.'.'.$this-&gt;path;
    }
}</pre><p>你会注意到，在这种情况下,你需要做一点工作,以删除该文件。在数据删除之前，你必须保存文件路径（因为它依赖于ID）。然后，一旦对象已经完全从数据库中删除，你就可以安全的删除文件（在数据删除之后）。</p>
<p><a rel="nofollow" href="http://www.newlifeclan.com/symfony/archives/199">(Doctrine)如何处理Doctrine文件上传</a>，首发于<a rel="nofollow" href="http://www.newlifeclan.com/symfony">Symfony中文教程</a>。</p>
]]></content:encoded>
			<wfw:commentRss>http://www.newlifeclan.com/symfony/archives/199/feed</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
