Made document editing possible

This commit is contained in:
Polonkai Gergely 2012-07-22 19:38:00 +02:00
parent 1b55b079f4
commit b82b4ffd34
136 changed files with 2734 additions and 1289 deletions

View File

@ -6,13 +6,17 @@ use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use KekRozsak\FrontBundle\Entity\Document;
use KekRozsak\FrontBundle\Form\Type\DocumentType;
use KekRozsak\FrontBundle\Extensions\Slugifier;
class DocumentController extends Controller
{
/**
* @Route("/dokumentum/{documentSlug}", name="KekRozsakFrontBundle_documentView")
* @Template()
*/
public function documentViewAction($documentSlug)
public function viewAction($documentSlug)
{
$docRepo = $this->getDoctrine()->getRepository('KekRozsakFrontBundle:Document');
if (!($document = $docRepo->findOneBySlug($documentSlug)))
@ -22,4 +26,86 @@ class DocumentController extends Controller
'document' => $document,
);
}
/**
* @Route("/dokumentumok/uj/{groupSlug}", name="KekRozsakFrontBundle_documentCreate", defaults={"groupslug"=""})
* @Template()
*/
public function createAction($groupSlug = '')
{
$groupRepo = $this->getDoctrine()->getRepository('KekRozsakFrontBundle:Group');
$group = $groupRepo->findOneBySlug($groupSlug);
/* TODO: make group fully optional */
$document = new Document();
$document->setSlug('n-a');
$form = $this->createForm(new DocumentType(), $document);
$request = $this->getRequest();
if ($request->getMethod() == 'POST')
{
$form->bindRequest($request);
if ($form->isValid())
{
$slugifier = new Slugifier();
$document->setSlug($slugifier->slugify($document->getTitle()));
$document->setCreatedAt(new \DateTime('now'));
$document->setCreatedBy($this->get('security.context')->getToken()->getUser());
if ($group)
{
$group->addDocument($document);
$document->addGroup($group);
}
$em = $this->getDoctrine()->getEntityManager();
$em->persist($document);
$em->flush();
/* TODO: return to group list if groupSlug is empty! */
return $this->redirect($this->generateUrl('KekRozsakFrontBundle_groupDocuments', array('groupSlug' => $group->getSlug())));
}
}
return array(
'defaultGroup' => $groupSlug,
'form' => $form->createView(),
);
}
/**
* @Route("/dokumentum/{documentSlug}/szerkesztes", name="KekRozsakFrontBundle_documentEdit")
* @Template()
*/
public function editAction($documentSlug)
{
$documentRepo = $this->getDoctrine()->getRepository('KekRozsakFrontBundle:Document');
if (!($document = $documentRepo->findOneBySlug($documentSlug)))
throw $this->createNotFoundException('A kért dokumentum nem létezik!');
$form = $this->createForm(new DocumentType(), $document);
$request = $this->getRequest();
if ($request->getMethod() == 'POST')
{
$form->bindRequest($request);
if ($form->isValid())
{
$slugifier = new Slugifier();
$document->setSlug($slugifier->slugify($document->getTitle()));
// TODO: add updatedAt, updatedBy, updateReason, etc.
$em = $this->getDoctrine()->getEntityManager();
$em->persist($document);
$em->flush();
return $this->redirect($this->generateUrl('KekRozsakFrontBundle_documentView', array('documentSlug' => $document->getSlug())));
}
}
return array(
'document' => $document,
'form' => $form->createView(),
);
}
}

View File

@ -4,6 +4,8 @@ namespace KekRozsak\FrontBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Bridge\Doctrine\Validator\Constraints as DoctrineAssert;
use KekRozsak\SecurityBundle\Entity\User;
use KekRozsak\FrontBundle\Entity\Group;
@ -12,6 +14,8 @@ use KekRozsak\FrontBundle\Entity\Group;
* KekRozsak\FrontBundle\Entity\Document
* @ORM\Entity
* @ORM\Table(name="documents")
* @DoctrineAssert\UniqueEntity(fields={"title"}, message="Ilyen című dokumentum már létezik. Kérlek válassz másikat!")
* @DoctrineAssert\UniqueEntity(fields={"slug"}, message="Ilyen című dokumentum már létezik. Kérlek válassz másikat!")
*/
class Document
{
@ -41,6 +45,7 @@ class Document
/**
* @var string $title
* @ORM\Column(type="string", length=150, unique=true, nullable=false)
* @Assert\NotBlank()
*/
protected $title;
@ -69,6 +74,7 @@ class Document
/**
* @var string $slug
* @ORM\Column(type="string", length=150, unique=true, nullable=false)
* @Assert\NotBlank()
*/
protected $slug;
@ -207,4 +213,88 @@ class Document
{
return $this->group;
}
/**
* @var KekRozsak\SecurityBundle\Entity\User $updatedBy
* @ORM\ManyToOne(targetEntity="KekRozsak\SecurityBundle\Entity\User")
*/
protected $updatedBy;
/**
* Set updatedBy
*
* @param KekRozsak\SecurityBundle\Entity\User $updatedBy
* @return Document
*/
public function setUpdatedBy(\KekRozsak\SecurityBundle\Entity\User $updatedBy = null)
{
$this->updatedBy = $updatedBy;
return $this;
}
/**
* Get updatedBy
*
* @return KekRozsak\SecurityBundle\Entity\User
*/
public function getUpdatedBy()
{
return $this->updatedBy;
}
/**
* @var DateTime $updatedAt
* @ORM\Column(type="datetime", nullable=true)
*/
protected $updatedAt;
/**
* Set updatedAt
*
* @param DateTime $updatedAt
* @return Document
*/
public function setUpdatedAt(\DateTime $updatedAt = null)
{
$this->updatedAt = $updatedAt;
return $this;
}
/**
* Get updatedAt
*
* @return DateTime
*/
public function getUpdatedAt()
{
return $this->updatedAt;
}
/**
* @var string updateReason
* @ORM\Column(type="text", nullable=true)
*/
protected $updateReason;
/**
* Set updateReason
*
* @param string $updateReason
* @return Document
*/
public function setUpdateReason($updateReason = null)
{
$this->updateReason = $updateReason;
return $this;
}
/**
* Get updateReason
*
* @return string
*/
public function getUpdateReason()
{
return $this->updateReason;
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace KekRozsak\FrontBundle\Form\Type;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
class DocumentType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('title', null, array(
'label' => 'A dokumentum címe',
));
$builder->add('content', 'ckeditor', array(
'label' => ' ',
));
/* TODO: possibility to add to other groups! */
}
public function getName()
{
return 'document';
}
}

View File

@ -0,0 +1,9 @@
{% extends '::main_template.html.twig' %}
{% block title %} - Dokumentum létrehozása{% endblock %}
{% block content %}
<h3>Új dokumentum létrehozása</h3>
<form method="post" action="{{ path('KekRozsakFrontBundle_documentCreate', { groupSlug: defaultGroup }) }}">
{{ form_widget(form) }}
<button type="submit">Mentés</button>
</form>
{% endblock content %}

View File

@ -1,7 +0,0 @@
{% extends '::main_template.html.twig' %}
{% block title %} - Dokumentum - {{ document.title }}{% endblock %}
{% block content %}
<h3>{{ document.title }}</h3>
{{ document.content }}
<div class="szerzo">{{ document.createdBy.displayName }}</div>
{% endblock content %}

View File

@ -0,0 +1,9 @@
{% extends '::main_template.html.twig' %}
{% block title %} - Dokumentum szerkesztése - {{ document.title }}{% endblock %}
{% block content %}
<h3>Dokumentum szerkesztése - {{ document.title }}</h3>
<form method="post" action="{{ path('KekRozsakFrontBundle_documentEdit', { documentSlug: document.slug }) }}">
{{ form_widget(form) }}
<button type="submit">Mentés</button>
</form>
{% endblock content %}

View File

@ -0,0 +1,9 @@
{% extends '::main_template.html.twig' %}
{% block title %} - Dokumentum - {{ document.title }}{% endblock %}
{% block content %}
<h3>
{{ document.title }}{% if document.createdBy == app.user %} [ <a href="{{ path('KekRozsakFrontBundle_documentEdit', { documentSlug: document.slug }) }}">Szerkesztés</a> ] {% endif %}
</h3>
{{ document.content|raw }}
<div class="szerzo">{{ document.createdBy.displayName }}</div>
{% endblock content %}

View File

@ -9,9 +9,25 @@
<li><a href="{{ path('KekRozsakFrontBundle_groupMembers', { groupSlug: group.slug }) }}">Tagok</a></li>
</ul>
<h3>{{ group.name }} - Dokumentumok</h3>
<ul>
<table>
<thead>
<tr>
<td colspan="2">Cím</td>
<td>Készítette</td>
</tr>
</thead>
<tbody>
{% for document in group.documents %}
<li><a href="{{ path('KekRozsakFrontBundle_documentView', { documentSlug: document.slug }) }}">{{ document.title }}</a></li>
<tr>
<td>[ikon]</td>
<td><a href="{{ path('KekRozsakFrontBundle_documentView', { documentSlug: document.slug }) }}">{{ document.title }}</a></td>
<td>
{{ document.createdBy.displayName }}<br />
{{ document.createdAt|date('Y-m-d H:i') }}
</td>
</tr>
{% endfor %}
</ul>
</tbody>
</table>
<a href="{{ path('KekRozsakFrontBundle_documentCreate', { groupSlug: group.slug }) }}">Új dokumentum</a>
{% endblock %}

View File

@ -3,7 +3,7 @@
"name": "jms/metadata",
"version": "1.1.1",
"version_normalized": "1.1.1.0",
"time": "2011-12-29 19:32:49",
"time": "2011-12-29 14:32:49",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/metadata",
@ -47,7 +47,7 @@
"name": "jms/cg",
"version": "1.0.0",
"version_normalized": "1.0.0.0",
"time": "2011-12-29 18:40:52",
"time": "2011-12-29 13:40:52",
"source": {
"type": "git",
"url": "git://github.com/schmittjoh/cg-library.git",
@ -89,7 +89,7 @@
"version": "1.0.0",
"version_normalized": "1.0.0.0",
"target-dir": "JMS/AopBundle",
"time": "2011-12-29 18:50:26",
"time": "2011-12-29 13:50:26",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/JMSAopBundle",
@ -133,7 +133,7 @@
"version": "1.1.0",
"version_normalized": "1.1.0.0",
"target-dir": "JMS/SecurityExtraBundle",
"time": "2011-12-29 22:38:12",
"time": "2011-12-29 17:38:12",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/JMSSecurityExtraBundle",
@ -178,7 +178,7 @@
"version": "1.0.1",
"version_normalized": "1.0.1.0",
"target-dir": "JMS/DiExtraBundle",
"time": "2012-02-24 14:01:54",
"time": "2012-02-24 09:01:54",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/JMSDiExtraBundle",
@ -221,7 +221,7 @@
"name": "doctrine/common",
"version": "2.2.2",
"version_normalized": "2.2.2.0",
"time": "2012-04-06 21:46:44",
"time": "2012-04-06 11:46:44",
"source": {
"type": "git",
"url": "https://github.com/doctrine/common",
@ -287,7 +287,7 @@
"name": "monolog/monolog",
"version": "1.1.0",
"version_normalized": "1.1.0.0",
"time": "2012-04-17 08:27:40",
"time": "2012-04-16 22:27:40",
"source": {
"type": "git",
"url": "https://github.com/Seldaek/monolog.git",
@ -337,7 +337,7 @@
"name": "twig/extensions",
"version": "dev-master",
"version_normalized": "9999999-dev",
"time": "2012-05-15 15:28:19",
"time": "2012-05-15 05:28:19",
"source": {
"type": "git",
"url": "https://github.com/fabpot/Twig-extensions",
@ -378,71 +378,12 @@
}
}
},
{
"name": "kriswallsmith/assetic",
"version": "dev-master",
"version_normalized": "9999999-dev",
"time": "2012-06-06 19:41:54",
"source": {
"type": "git",
"url": "http://github.com/kriswallsmith/assetic.git",
"reference": "d6f89a3170c5280ad554347dc113eb25fdf00ad7"
},
"dist": {
"type": "zip",
"url": "https://github.com/kriswallsmith/assetic/zipball/d6f89a3170c5280ad554347dc113eb25fdf00ad7",
"reference": "d6f89a3170c5280ad554347dc113eb25fdf00ad7",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"symfony/process": "2.1.*"
},
"require-dev": {
"twig/twig": ">=1.6.0,<2.0",
"leafo/lessphp": "*"
},
"suggest": {
"twig/twig": "Assetic provides the integration with the Twig templating engine",
"leafo/lessphp": "Assetic provides the integration with the lessphp LESS compiler"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
}
},
"installation-source": "source",
"license": [
"MIT"
],
"authors": [
{
"name": "Kris Wallsmith",
"email": "kris.wallsmith@gmail.com",
"homepage": "http://kriswallsmith.net/",
"role": null
}
],
"description": "Asset Management for PHP",
"homepage": "https://github.com/kriswallsmith/assetic",
"keywords": [
"assets",
"compression",
"minification"
],
"autoload": {
"psr-0": {
"Assetic": "src/"
}
}
},
{
"name": "doctrine/doctrine-bundle",
"version": "dev-master",
"version_normalized": "9999999-dev",
"target-dir": "Doctrine/Bundle/DoctrineBundle",
"time": "2012-07-02 04:42:17",
"time": "2012-07-01 18:42:17",
"source": {
"type": "git",
"url": "git://github.com/doctrine/DoctrineBundle.git",
@ -516,7 +457,7 @@
"name": "doctrine/orm",
"version": "2.2.x-dev",
"version_normalized": "2.2.9999999.9999999-dev",
"time": "2012-07-06 01:48:00",
"time": "2012-07-05 15:48:00",
"source": {
"type": "git",
"url": "git://github.com/doctrine/doctrine2.git",
@ -581,7 +522,7 @@
"name": "doctrine/dbal",
"version": "2.2.x-dev",
"version_normalized": "2.2.9999999.9999999-dev",
"time": "2012-07-07 12:30:35",
"time": "2012-07-07 02:30:35",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal",
@ -647,7 +588,7 @@
"version": "dev-master",
"version_normalized": "9999999-dev",
"target-dir": "Sensio/Bundle/GeneratorBundle",
"time": "2012-07-12 18:04:48",
"time": "2012-07-12 08:04:48",
"source": {
"type": "git",
"url": "https://github.com/sensio/SensioGeneratorBundle",
@ -695,68 +636,12 @@
}
}
},
{
"name": "symfony/assetic-bundle",
"version": "dev-master",
"version_normalized": "9999999-dev",
"target-dir": "Symfony/Bundle/AsseticBundle",
"time": "2012-07-12 00:51:17",
"source": {
"type": "git",
"url": "https://github.com/symfony/AsseticBundle",
"reference": "ed933dcfa45f00b6bc6d7727007403f3ff429e5a"
},
"dist": {
"type": "zip",
"url": "https://github.com/symfony/AsseticBundle/zipball/ed933dcfa45f00b6bc6d7727007403f3ff429e5a",
"reference": "ed933dcfa45f00b6bc6d7727007403f3ff429e5a",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"symfony/framework-bundle": "2.1.*",
"kriswallsmith/assetic": "1.1.*"
},
"suggest": {
"symfony/twig-bundle": "2.1.*"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "2.1.x-dev"
}
},
"installation-source": "source",
"license": [
"MIT"
],
"authors": [
{
"name": "Kris Wallsmith",
"email": "kris.wallsmith@gmail.com",
"homepage": "http://kriswallsmith.net/",
"role": null
}
],
"description": "Integrates Assetic into Symfony2",
"homepage": "https://github.com/symfony/AsseticBundle",
"keywords": [
"assets",
"compression",
"minification"
],
"autoload": {
"psr-0": {
"Symfony\\Bundle\\AsseticBundle": ""
}
}
},
{
"name": "symfony/monolog-bundle",
"version": "dev-master",
"version_normalized": "9999999-dev",
"target-dir": "Symfony/Bundle/MonologBundle",
"time": "2012-06-29 23:48:07",
"time": "2012-06-29 13:48:07",
"source": {
"type": "git",
"url": "https://github.com/symfony/MonologBundle",
@ -816,7 +701,7 @@
"version": "dev-master",
"version_normalized": "9999999-dev",
"target-dir": "Sensio/Bundle/FrameworkExtraBundle",
"time": "2012-07-11 17:13:52",
"time": "2012-07-11 07:13:52",
"source": {
"type": "git",
"url": "https://github.com/sensio/SensioFrameworkExtraBundle",
@ -861,124 +746,11 @@
}
}
},
{
"name": "twig/twig",
"version": "dev-master",
"version_normalized": "9999999-dev",
"time": "2012-07-15 17:42:44",
"source": {
"type": "git",
"url": "git://github.com/fabpot/Twig.git",
"reference": "d45664045194ef62573c153c424508527105041e"
},
"dist": {
"type": "zip",
"url": "https://github.com/fabpot/Twig/zipball/d45664045194ef62573c153c424508527105041e",
"reference": "d45664045194ef62573c153c424508527105041e",
"shasum": ""
},
"require": {
"php": ">=5.2.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"installation-source": "source",
"license": [
"BSD-3"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": null,
"role": null
},
{
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"homepage": null,
"role": null
}
],
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "http://twig.sensiolabs.org",
"keywords": [
"templating"
],
"autoload": {
"psr-0": {
"Twig_": "lib/"
}
}
},
{
"name": "symfony/swiftmailer-bundle",
"version": "dev-master",
"version_normalized": "9999999-dev",
"target-dir": "Symfony/Bundle/SwiftmailerBundle",
"time": "2012-07-05 05:33:34",
"source": {
"type": "git",
"url": "https://github.com/symfony/SwiftmailerBundle",
"reference": "v2.1.0-BETA3"
},
"dist": {
"type": "zip",
"url": "https://github.com/symfony/SwiftmailerBundle/zipball/v2.1.0-BETA3",
"reference": "v2.1.0-BETA3",
"shasum": ""
},
"require": {
"php": ">=5.3.2",
"symfony/swiftmailer-bridge": "2.1.*",
"swiftmailer/swiftmailer": ">=4.2.0,<4.3-dev"
},
"require-dev": {
"symfony/dependency-injection": "2.1.*",
"symfony/http-kernel": "2.1.*",
"symfony/config": "2.1.*"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
}
},
"installation-source": "source",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": null,
"role": null
},
{
"name": "Symfony Community",
"email": null,
"homepage": "http://symfony.com/contributors",
"role": null
}
],
"description": "Symfony SwiftmailerBundle",
"homepage": "http://symfony.com",
"autoload": {
"psr-0": {
"Symfony\\Bundle\\SwiftmailerBundle": ""
}
}
},
{
"name": "swiftmailer/swiftmailer",
"version": "dev-master",
"version_normalized": "9999999-dev",
"time": "2012-07-13 06:47:17",
"time": "2012-07-12 20:47:17",
"source": {
"type": "git",
"url": "git://github.com/swiftmailer/swiftmailer.git",
@ -1034,7 +806,7 @@
"version": "dev-master",
"version_normalized": "9999999-dev",
"target-dir": "Sensio/Bundle/DistributionBundle",
"time": "2012-07-15 04:24:10",
"time": "2012-07-14 18:24:10",
"source": {
"type": "git",
"url": "https://github.com/sensio/SensioDistributionBundle",
@ -1073,12 +845,71 @@
}
}
},
{
"name": "symfony/swiftmailer-bundle",
"version": "dev-master",
"version_normalized": "9999999-dev",
"target-dir": "Symfony/Bundle/SwiftmailerBundle",
"time": "2012-07-19 01:55:12",
"source": {
"type": "git",
"url": "https://github.com/symfony/SwiftmailerBundle",
"reference": "65e6079443c3cd413012e146aa74307f18671f42"
},
"dist": {
"type": "zip",
"url": "https://github.com/symfony/SwiftmailerBundle/zipball/65e6079443c3cd413012e146aa74307f18671f42",
"reference": "65e6079443c3cd413012e146aa74307f18671f42",
"shasum": ""
},
"require": {
"php": ">=5.3.2",
"symfony/swiftmailer-bridge": "2.1.*",
"swiftmailer/swiftmailer": ">=4.2.0,<4.3-dev"
},
"require-dev": {
"symfony/dependency-injection": "2.1.*",
"symfony/http-kernel": "2.1.*",
"symfony/config": "2.1.*"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "2.1-dev"
}
},
"installation-source": "source",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": null,
"role": null
},
{
"name": "Symfony Community",
"email": null,
"homepage": "http://symfony.com/contributors",
"role": null
}
],
"description": "Symfony SwiftmailerBundle",
"homepage": "http://symfony.com",
"autoload": {
"psr-0": {
"Symfony\\Bundle\\SwiftmailerBundle": ""
}
}
},
{
"name": "egeloen/ckeditor-bundle",
"version": "dev-master",
"version_normalized": "9999999-dev",
"target-dir": "Ivory/CKEditorBundle",
"time": "2012-05-28 13:16:47",
"time": "2012-05-28 11:16:47",
"source": {
"type": "git",
"url": "https://github.com/egeloen/IvoryCKEditorBundle",
@ -1118,19 +949,188 @@
}
},
{
"name": "symfony/symfony",
"name": "kriswallsmith/assetic",
"version": "dev-master",
"version_normalized": "9999999-dev",
"time": "2012-07-17 09:07:53",
"time": "2012-07-20 10:33:33",
"source": {
"type": "git",
"url": "git://github.com/symfony/symfony.git",
"reference": "f52ce6178243e4b11aa09bde147f684d596fb120"
"url": "http://github.com/kriswallsmith/assetic.git",
"reference": "5c1c9b658b732a980312e8f29cec4e175c2bb6b2"
},
"dist": {
"type": "zip",
"url": "https://github.com/symfony/symfony/zipball/f52ce6178243e4b11aa09bde147f684d596fb120",
"reference": "f52ce6178243e4b11aa09bde147f684d596fb120",
"url": "https://github.com/kriswallsmith/assetic/zipball/5c1c9b658b732a980312e8f29cec4e175c2bb6b2",
"reference": "5c1c9b658b732a980312e8f29cec4e175c2bb6b2",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"symfony/process": "2.1.*"
},
"require-dev": {
"twig/twig": ">=1.6.0,<2.0",
"leafo/lessphp": "*"
},
"suggest": {
"twig/twig": "Assetic provides the integration with the Twig templating engine",
"leafo/lessphp": "Assetic provides the integration with the lessphp LESS compiler"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.1-dev"
}
},
"installation-source": "source",
"license": [
"MIT"
],
"authors": [
{
"name": "Kris Wallsmith",
"email": "kris.wallsmith@gmail.com",
"homepage": "http://kriswallsmith.net/",
"role": null
}
],
"description": "Asset Management for PHP",
"homepage": "https://github.com/kriswallsmith/assetic",
"keywords": [
"assets",
"compression",
"minification"
],
"autoload": {
"psr-0": {
"Assetic": "src/"
}
}
},
{
"name": "symfony/assetic-bundle",
"version": "dev-master",
"version_normalized": "9999999-dev",
"target-dir": "Symfony/Bundle/AsseticBundle",
"time": "2012-07-20 19:34:07",
"source": {
"type": "git",
"url": "https://github.com/symfony/AsseticBundle",
"reference": "e5e9a56a872d9e1f305d14cd8296bf77888388af"
},
"dist": {
"type": "zip",
"url": "https://github.com/symfony/AsseticBundle/zipball/e5e9a56a872d9e1f305d14cd8296bf77888388af",
"reference": "e5e9a56a872d9e1f305d14cd8296bf77888388af",
"shasum": ""
},
"require": {
"php": ">=5.3.0",
"symfony/framework-bundle": "2.1.*",
"kriswallsmith/assetic": "1.1.*"
},
"suggest": {
"symfony/twig-bundle": "2.1.*"
},
"type": "symfony-bundle",
"extra": {
"branch-alias": {
"dev-master": "2.1.x-dev"
}
},
"installation-source": "source",
"license": [
"MIT"
],
"authors": [
{
"name": "Kris Wallsmith",
"email": "kris.wallsmith@gmail.com",
"homepage": "http://kriswallsmith.net/",
"role": null
}
],
"description": "Integrates Assetic into Symfony2",
"homepage": "https://github.com/symfony/AsseticBundle",
"keywords": [
"assets",
"compression",
"minification"
],
"autoload": {
"psr-0": {
"Symfony\\Bundle\\AsseticBundle": ""
}
}
},
{
"name": "twig/twig",
"version": "dev-master",
"version_normalized": "9999999-dev",
"time": "2012-07-20 12:41:38",
"source": {
"type": "git",
"url": "git://github.com/fabpot/Twig.git",
"reference": "8a84838798e45424c5fe2d87149db6855ae037bf"
},
"dist": {
"type": "zip",
"url": "https://github.com/fabpot/Twig/zipball/8a84838798e45424c5fe2d87149db6855ae037bf",
"reference": "8a84838798e45424c5fe2d87149db6855ae037bf",
"shasum": ""
},
"require": {
"php": ">=5.2.4"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"installation-source": "source",
"license": [
"BSD-3"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com",
"homepage": null,
"role": null
},
{
"name": "Armin Ronacher",
"email": "armin.ronacher@active-4.com",
"homepage": null,
"role": null
}
],
"description": "Twig, the flexible, fast, and secure template language for PHP",
"homepage": "http://twig.sensiolabs.org",
"keywords": [
"templating"
],
"autoload": {
"psr-0": {
"Twig_": "lib/"
}
}
},
{
"name": "symfony/symfony",
"version": "dev-master",
"version_normalized": "9999999-dev",
"time": "2012-07-21 11:16:18",
"source": {
"type": "git",
"url": "git://github.com/symfony/symfony.git",
"reference": "6c256b01b087f94a4ec04487d875fe81375eb6c1"
},
"dist": {
"type": "zip",
"url": "https://github.com/symfony/symfony/zipball/6c256b01b087f94a4ec04487d875fe81375eb6c1",
"reference": "6c256b01b087f94a4ec04487d875fe81375eb6c1",
"shasum": ""
},
"require": {

View File

@ -0,0 +1,4 @@
phpunit.xml
vendor/
composer.phar
composer.lock

View File

@ -1,5 +1,6 @@
{
"name": "kriswallsmith/assetic",
"minimum-stability": "dev",
"description": "Asset Management for PHP",
"keywords": ["assets", "compression", "minification"],
"homepage": "https://github.com/kriswallsmith/assetic",
@ -20,6 +21,7 @@
"twig/twig": ">=1.6.0,<2.0",
"leafo/lessphp": "*"
},
"minimum-stability": "dev",
"suggest": {
"twig/twig": "Assetic provides the integration with the Twig templating engine",
"leafo/lessphp": "Assetic provides the integration with the lessphp LESS compiler"

View File

@ -25,6 +25,7 @@ use Symfony\Component\Process\ProcessBuilder;
class CompassFilter implements FilterInterface
{
private $compassPath;
private $rubyPath;
private $scss;
// sass options
@ -50,9 +51,10 @@ class CompassFilter implements FilterInterface
private $generatedImagesPath;
private $httpJavascriptsPath;
public function __construct($compassPath = '/usr/bin/compass')
public function __construct($compassPath = '/usr/bin/compass', $rubyPath = null)
{
$this->compassPath = $compassPath;
$this->rubyPath = $rubyPath;
$this->cacheLocation = sys_get_temp_dir();
if ('cli' !== php_sapi_name()) {
@ -133,6 +135,11 @@ class CompassFilter implements FilterInterface
$this->plugins[] = $plugin;
}
public function setLoadPaths(array $loadPaths)
{
$this->loadPaths = $loadPaths;
}
public function addLoadPath($loadPath)
{
$this->loadPaths[] = $loadPath;
@ -171,11 +178,16 @@ class CompassFilter implements FilterInterface
// compass does not seems to handle symlink, so we use realpath()
$tempDir = realpath(sys_get_temp_dir());
$pb = new ProcessBuilder(array(
$compassProcessArgs = array(
$this->compassPath,
'compile',
$tempDir,
));
);
if (null !== $this->rubyPath) {
array_unshift($compassProcessArgs, $this->rubyPath);
}
$pb = new ProcessBuilder($compassProcessArgs);
$pb->inheritEnvironmentVariables();
if ($this->force) {
@ -330,12 +342,12 @@ class CompassFilter implements FilterInterface
// does we have an associative array ?
if (count(array_filter(array_keys($array), "is_numeric")) != count($array)) {
foreach($array as $name => $value) {
foreach ($array as $name => $value) {
$output[] = sprintf(' :%s => "%s"', $name, addcslashes($value, '\\'));
}
$output = "{\n".implode(",\n", $output)."\n}";
} else {
foreach($array as $name => $value) {
foreach ($array as $name => $value) {
$output[] = sprintf(' "%s"', addcslashes($value, '\\'));
}
$output = "[\n".implode(",\n", $output)."\n]";

View File

@ -30,6 +30,7 @@ class SassFilter implements FilterInterface
const STYLE_COMPRESSED = 'compressed';
private $sassPath;
private $rubyPath;
private $unixNewlines;
private $scss;
private $style;
@ -41,9 +42,10 @@ class SassFilter implements FilterInterface
private $noCache;
private $compass;
public function __construct($sassPath = '/usr/bin/sass')
public function __construct($sassPath = '/usr/bin/sass', $rubyPath = null)
{
$this->sassPath = $sassPath;
$this->rubyPath = $rubyPath;
$this->cacheLocation = realpath(sys_get_temp_dir());
}
@ -99,7 +101,12 @@ class SassFilter implements FilterInterface
public function filterLoad(AssetInterface $asset)
{
$pb = new ProcessBuilder(array($this->sassPath));
$sassProcessArgs = array($this->sassPath);
if (null !== $this->rubyPath) {
array_unshift($sassProcessArgs, $this->rubyPath);
}
$pb = new ProcessBuilder($sassProcessArgs);
$root = $asset->getSourceRoot();
$path = $asset->getSourcePath();

View File

@ -19,9 +19,9 @@ namespace Assetic\Filter\Sass;
*/
class ScssFilter extends SassFilter
{
public function __construct($sassPath = '/usr/bin/sass')
public function __construct($sassPath = '/usr/bin/sass', $rubyPath = null)
{
parent::__construct($sassPath);
parent::__construct($sassPath, $rubyPath);
$this->setScss(true);
}

View File

@ -80,11 +80,10 @@ abstract class BaseCompressorFilter implements FilterInterface
// input and output files
$tempDir = realpath(sys_get_temp_dir());
$hash = substr(sha1(time().rand(11111, 99999)), 0, 7);
$input = $tempDir.DIRECTORY_SEPARATOR.$hash.'.'.$type;
$output = $tempDir.DIRECTORY_SEPARATOR.$hash.'-min.'.$type;
$input = tempnam($tempDir, 'YUI-IN-');
$output = tempnam($tempDir, 'YUI-OUT-');
file_put_contents($input, $content);
$pb->add('-o')->add($output)->add($input);
$pb->add('-o')->add($output)->add('--type')->add($type)->add($input);
$proc = $pb->getProcess();
$code = $proc->run();

View File

@ -9,7 +9,7 @@
* file that was distributed with this source code.
*/
if (!$loader = @include __DIR__.'/../vendor/.composer/autoload.php') {
if (!$loader = @include __DIR__.'/../vendor/autoload.php') {
die('You must set up the project dependencies, run the following commands:'.PHP_EOL.
'curl -s http://getcomposer.org/installer | php'.PHP_EOL.
'php composer.phar install'.PHP_EOL);

View File

@ -57,7 +57,7 @@ class DumpCommand extends ContainerAwareCommand
protected function execute(InputInterface $input, OutputInterface $output)
{
$output->writeln(sprintf('Dumping all <comment>%s</comment> assets.', $input->getOption('env')));
$output->writeln(sprintf('Debug mode is <comment>%s</comment>.', $input->getOption('no-debug') ? 'off' : 'on'));
$output->writeln(sprintf('Debug mode is <comment>%s</comment>.', $this->am->isDebug() ? 'on' : 'off'));
$output->writeln('');
if (!$input->getOption('watch')) {

View File

@ -22,6 +22,7 @@
<service id="assetic.filter.compass" class="%assetic.filter.compass.class%">
<tag name="assetic.filter" alias="compass" />
<argument>%assetic.filter.compass.bin%</argument>
<argument>%assetic.ruby.bin%</argument>
<call method="setDebugInfo"><argument>%assetic.filter.compass.debug%</argument></call>
<call method="setStyle"><argument>%assetic.filter.compass.style%</argument></call>
<call method="setImagesDir"><argument>%assetic.filter.compass.images_dir%</argument></call>

View File

@ -15,6 +15,7 @@
<service id="assetic.filter.sass" class="%assetic.filter.sass.class%">
<tag name="assetic.filter" alias="sass" />
<argument>%assetic.filter.sass.bin%</argument>
<argument>%assetic.ruby.bin%</argument>
<call method="setStyle"><argument>%assetic.filter.sass.style%</argument></call>
<call method="setCompass"><argument>%assetic.filter.sass.compass%</argument></call>
</service>

View File

@ -15,6 +15,7 @@
<service id="assetic.filter.scss" class="%assetic.filter.scss.class%">
<tag name="assetic.filter" alias="scss" />
<argument>%assetic.filter.scss.sass%</argument>
<argument>%assetic.ruby.bin%</argument>
<call method="setStyle"><argument>%assetic.filter.scss.style%</argument></call>
<call method="setCompass"><argument>%assetic.filter.scss.compass%</argument></call>
</service>

View File

@ -46,7 +46,13 @@
{% endfor %}
<p>
<pre>{{ message.body|e('html', message.charset)|convert_encoding('UTF-8', message.charset) }}</pre>
<pre>
{%- if message.charset %}
{{- message.body|e('html', message.charset)|convert_encoding('UTF-8', message.charset) }}
{%- else %}
{{- message.body|e('html') }}
{%- endif -%}
</pre>
</p>
{% endfor %}
{% endif %}

View File

@ -13,14 +13,15 @@
configuration (i.e. `config.yml`), merging could yield a set of base URL's
for multiple environments.
* The priorities for the built-in listeners have changed:
* The priorities for the built-in listeners have changed.
```
2.0 2.1
security.firewall request 64 8
locale listener early_request 253 255
request -1 16
router listener early_request 255 128
request 10 32
security.firewall kernel.request 64 8
locale listener kernel.request 0 16
router listener early_request 255 n/a
request 0 32
```
### Doctrine
@ -106,7 +107,7 @@
if ($locale = $request->attributes->get('_locale')) {
$request->getSession()->set('_locale', $locale);
} else {
$request->setDefaultLocale($request->getSession()->get('_locale', $this->defaultLocale));
$request->setLocale($request->getSession()->get('_locale', $this->defaultLocale));
}
}
@ -1053,10 +1054,25 @@
$registry->addType($registry->resolveType(new MyFormType()));
```
* The method `renderBlock()` of the helper for the PHP Templating component was
deprecated and will be removed in Symfony 2.3. You should use `block()` instead.
Before:
```
<?php echo $view['form']->renderBlock('widget_attributes') ?>
```
After:
```
<?php echo $view['form']->block('widget_attributes') ?>
```
### Validator
* The methods `setMessage()`, `getMessageTemplate()` and
`getMessageParameters()` in the Constraint class were deprecated and will
`getMessageParameters()` in the `ConstraintValidator` class were deprecated and will
be removed in Symfony 2.3.
If you have implemented custom validators, you should use the

View File

@ -9,3 +9,4 @@ CHANGELOG
* DoctrineOrmTypeGuesser now guesses "collection" for array Doctrine type
* DoctrineType now caches its choice lists in order to improve performance
* DoctrineType now uses ManagerRegistry::getManagerForClass() if the option "em" is not set
* UniqueEntity validation constraint now accepts a "repositoryMethod" option that will be used to check for uniqueness instead of the default "findBy"

View File

@ -12,11 +12,12 @@
namespace Symfony\Bridge\Doctrine\Form;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\Common\Persistence\Mapping\MappingException;
use Doctrine\ORM\Mapping\MappingException as LegacyMappingException;
use Symfony\Component\Form\FormTypeGuesserInterface;
use Symfony\Component\Form\Guess\Guess;
use Symfony\Component\Form\Guess\TypeGuess;
use Symfony\Component\Form\Guess\ValueGuess;
use Doctrine\ORM\Mapping\MappingException;
class DoctrineOrmTypeGuesser implements FormTypeGuesserInterface
{
@ -145,6 +146,8 @@ class DoctrineOrmTypeGuesser implements FormTypeGuesserInterface
return $this->cache[$class] = array($em->getClassMetadata($class), $name);
} catch (MappingException $e) {
// not an entity or mapped super class
} catch (LegacyMappingException $e) {
// not an entity or mapped super class, using Doctrine ORM 2.2
}
}
}

View File

@ -114,7 +114,7 @@ abstract class DoctrineType extends AbstractType
return $choiceListCache[$hash];
};
$emFilter = function (Options $options, $em) use ($registry) {
$emNormalizer = function (Options $options, $em) use ($registry) {
/* @var ManagerRegistry $registry */
if (null !== $em) {
return $registry->getManager($em);
@ -134,8 +134,8 @@ abstract class DoctrineType extends AbstractType
'group_by' => null,
));
$resolver->setFilters(array(
'em' => $emFilter,
$resolver->setNormalizers(array(
'em' => $emNormalizer,
));
}

View File

@ -47,6 +47,50 @@ class UniqueValidatorTest extends DoctrineOrmTestCase
return $registry;
}
protected function createRepositoryMock()
{
$repository = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectRepository')
->setMethods(array('findByCustom', 'find', 'findAll', 'findOneBy', 'findBy', 'getClassName'))
->getMock()
;
return $repository;
}
protected function createEntityManagerMock($repositoryMock)
{
$em = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')
->getMock()
;
$em->expects($this->any())
->method('getRepository')
->will($this->returnValue($repositoryMock))
;
$classMetadata = $this->getMock('Doctrine\Common\Persistence\Mapping\ClassMetadata');
$classMetadata
->expects($this->any())
->method('hasField')
->will($this->returnValue(true))
;
$refl = $this->getMockBuilder('Doctrine\Common\Reflection\StaticReflectionProperty')
->disableOriginalConstructor()
->getMock()
;
$refl
->expects($this->any())
->method('getValue')
->will($this->returnValue(true))
;
$classMetadata->reflFields = array('name' => $refl);
$em->expects($this->any())
->method('getClassMetadata')
->will($this->returnValue($classMetadata))
;
return $em;
}
protected function createMetadataFactoryMock($metadata)
{
$metadataFactory = $this->getMock('Symfony\Component\Validator\Mapping\ClassMetadataFactoryInterface');
@ -69,7 +113,7 @@ class UniqueValidatorTest extends DoctrineOrmTestCase
return $validatorFactory;
}
public function createValidator($entityManagerName, $em, $validateClass = null, $uniqueFields = null, $errorPath = null)
public function createValidator($entityManagerName, $em, $validateClass = null, $uniqueFields = null, $errorPath = null, $repositoryMethod = 'findBy')
{
if (!$validateClass) {
$validateClass = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIdentEntity';
@ -83,7 +127,13 @@ class UniqueValidatorTest extends DoctrineOrmTestCase
$uniqueValidator = new UniqueEntityValidator($registry);
$metadata = new ClassMetadata($validateClass);
$metadata->addConstraint(new UniqueEntity(array('fields' => $uniqueFields, 'em' => $entityManagerName, 'errorPath' => $errorPath)));
$constraint = new UniqueEntity(array(
'fields' => $uniqueFields,
'em' => $entityManagerName,
'errorPath' => $errorPath,
'repositoryMethod' => $repositoryMethod
));
$metadata->addConstraint($constraint);
$metadataFactory = $this->createMetadataFactoryMock($metadata);
$validatorFactory = $this->createValidatorFactory($uniqueValidator);
@ -194,6 +244,23 @@ class UniqueValidatorTest extends DoctrineOrmTestCase
$this->assertEquals(1, $violationsList->count(), 'Violation found on entity with conflicting entity existing in the database.');
}
public function testValidateUniquenessUsingCustomRepositoryMethod()
{
$entityManagerName = 'foo';
$repository = $this->createRepositoryMock();
$repository->expects($this->once())
->method('findByCustom')
->will($this->returnValue(array()))
;
$em = $this->createEntityManagerMock($repository);
$validator = $this->createValidator($entityManagerName, $em, null, array(), null, 'findByCustom');
$entity1 = new SingleIdentEntity(1, 'foo');
$violationsList = $validator->validate($entity1);
$this->assertEquals(0, $violationsList->count(), 'Violation is using custom repository method.');
}
/**
* @group GH-1635
*/

View File

@ -24,6 +24,7 @@ class UniqueEntity extends Constraint
public $message = 'This value is already used.';
public $service = 'doctrine.orm.validator.unique';
public $em = null;
public $repositoryMethod = 'findBy';
public $fields = array();
public $errorPath = null;

View File

@ -100,7 +100,7 @@ class UniqueEntityValidator extends ConstraintValidator
}
$repository = $em->getRepository($className);
$result = $repository->findBy($criteria);
$result = $repository->{$constraint->repositoryMethod}($criteria);
/* If the result is a MongoCursor, it must be advanced to the first
* element. Rewinding should have no ill effect if $result is another

View File

@ -12,11 +12,9 @@
namespace Symfony\Bridge\Twig\Extension;
use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Bridge\Twig\Form\TwigRendererInterface;
use Symfony\Component\Form\FormViewInterface;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Util\FormUtil;
/**
* FormExtension extends Twig with form capabilities.
@ -26,21 +24,17 @@ use Symfony\Component\Form\Util\FormUtil;
*/
class FormExtension extends \Twig_Extension
{
protected $csrfProvider;
protected $resources;
protected $blocks;
protected $environment;
protected $themes;
protected $varStack;
protected $template;
/**
* This property is public so that it can be accessed directly from compiled
* templates without having to call a getter, which slightly decreases performance.
*
* @var \Symfony\Component\Form\FormRendererInterface
*/
public $renderer;
public function __construct(CsrfProviderInterface $csrfProvider = null, array $resources = array())
public function __construct(TwigRendererInterface $renderer)
{
$this->csrfProvider = $csrfProvider;
$this->themes = new \SplObjectStorage();
$this->varStack = array();
$this->blocks = new \SplObjectStorage();
$this->resources = $resources;
$this->renderer = $renderer;
}
/**
@ -48,25 +42,11 @@ class FormExtension extends \Twig_Extension
*/
public function initRuntime(\Twig_Environment $environment)
{
$this->environment = $environment;
$this->renderer->setEnvironment($environment);
}
/**
* Sets a theme for a given view.
*
* @param FormView $view A FormView instance
* @param array|string $resources An array of resource names|a resource name
*/
public function setTheme(FormView $view, $resources)
{
$this->themes->attach($view, (array) $resources);
$this->blocks = new \SplObjectStorage();
}
/**
* Returns the token parser instance to add to the existing list.
*
* @return array An array of Twig_TokenParser instances
* {@inheritdoc}
*/
public function getTokenParsers()
{
@ -76,305 +56,39 @@ class FormExtension extends \Twig_Extension
);
}
/**
* {@inheritdoc}
*/
public function getFunctions()
{
return array(
'form_enctype' => new \Twig_Function_Method($this, 'renderEnctype', array('is_safe' => array('html'))),
'form_widget' => new \Twig_Function_Method($this, 'renderWidget', array('is_safe' => array('html'))),
'form_errors' => new \Twig_Function_Method($this, 'renderErrors', array('is_safe' => array('html'))),
'form_label' => new \Twig_Function_Method($this, 'renderLabel', array('is_safe' => array('html'))),
'form_row' => new \Twig_Function_Method($this, 'renderRow', array('is_safe' => array('html'))),
'form_rest' => new \Twig_Function_Method($this, 'renderRest', array('is_safe' => array('html'))),
'csrf_token' => new \Twig_Function_Method($this, 'getCsrfToken'),
'_form_is_choice_group' => new \Twig_Function_Method($this, 'isChoiceGroup', array('is_safe' => array('html'))),
'_form_is_choice_selected' => new \Twig_Function_Method($this, 'isChoiceSelected', array('is_safe' => array('html'))),
'form_enctype' => new \Twig_Function_Method($this, 'renderer->renderEnctype', array('is_safe' => array('html'))),
'form_widget' => new \Twig_Function_Method($this, 'renderer->renderWidget', array('is_safe' => array('html'))),
'form_errors' => new \Twig_Function_Method($this, 'renderer->renderErrors', array('is_safe' => array('html'))),
'form_label' => new \Twig_Function_Method($this, 'renderer->renderLabel', array('is_safe' => array('html'))),
'form_row' => new \Twig_Function_Method($this, 'renderer->renderRow', array('is_safe' => array('html'))),
'form_rest' => new \Twig_Function_Method($this, 'renderer->renderRest', array('is_safe' => array('html'))),
'csrf_token' => new \Twig_Function_Method($this, 'renderer->renderCsrfToken'),
'_form_is_choice_group' => new \Twig_Function_Method($this, 'renderer->isChoiceGroup', array('is_safe' => array('html'))),
'_form_is_choice_selected' => new \Twig_Function_Method($this, 'renderer->isChoiceSelected', array('is_safe' => array('html'))),
);
}
/**
* {@inheritdoc}
*/
public function getFilters()
{
return array(
'humanize' => new \Twig_Filter_Function(__NAMESPACE__.'\humanize'),
'humanize' => new \Twig_Filter_Method($this, 'renderer->humanize'),
);
}
public function isChoiceGroup($label)
{
return FormUtil::isChoiceGroup($label);
}
public function isChoiceSelected(FormView $view, ChoiceView $choice)
{
return FormUtil::isChoiceSelected($choice->getValue(), $view->getVar('value'));
}
/**
* Renders the HTML enctype in the form tag, if necessary
*
* Example usage in Twig templates:
*
* <form action="..." method="post" {{ form_enctype(form) }}>
*
* @param FormView $view The view for which to render the encoding type
*
* @return string The html markup
*/
public function renderEnctype(FormView $view)
{
return $this->render($view, 'enctype');
}
/**
* Renders a row for the view.
*
* @param FormView $view The view to render as a row
* @param array $variables An array of variables
*
* @return string The html markup
*/
public function renderRow(FormView $view, array $variables = array())
{
return $this->render($view, 'row', $variables);
}
/**
* Renders views which have not already been rendered.
*
* @param FormView $view The parent view
* @param array $variables An array of variables
*
* @return string The html markup
*/
public function renderRest(FormView $view, array $variables = array())
{
return $this->render($view, 'rest', $variables);
}
/**
* Renders the HTML for a given view
*
* Example usage in Twig:
*
* {{ form_widget(view) }}
*
* You can pass options during the call:
*
* {{ form_widget(view, {'attr': {'class': 'foo'}}) }}
*
* {{ form_widget(view, {'separator': '+++++'}) }}
*
* @param FormView $view The view to render
* @param array $variables Additional variables passed to the template
*
* @return string The html markup
*/
public function renderWidget(FormView $view, array $variables = array())
{
return $this->render($view, 'widget', $variables);
}
/**
* Renders the errors of the given view
*
* @param FormView $view The view to render the errors for
*
* @return string The html markup
*/
public function renderErrors(FormView $view)
{
return $this->render($view, 'errors');
}
/**
* Renders the label of the given view
*
* @param FormView $view The view to render the label for
* @param string $label Label name
* @param array $variables Additional variables passed to the template
*
* @return string The html markup
*/
public function renderLabel(FormView $view, $label = null, array $variables = array())
{
if ($label !== null) {
$variables += array('label' => $label);
}
return $this->render($view, 'label', $variables);
}
/**
* Renders a template.
*
* 1. This function first looks for a block named "_<view id>_<section>",
* 2. if such a block is not found the function will look for a block named
* "<type name>_<section>",
* 3. the type name is recursively replaced by the parent type name until a
* corresponding block is found
*
* @param FormView $view The form view
* @param string $section The section to render (i.e. 'row', 'widget', 'label', ...)
* @param array $variables Additional variables
*
* @return string The html markup
*
* @throws FormException if no template block exists to render the given section of the view
*/
protected function render(FormView $view, $section, array $variables = array())
{
$mainTemplate = in_array($section, array('widget', 'row'));
if ($mainTemplate && $view->isRendered()) {
return '';
}
if (null === $this->template) {
$this->template = reset($this->resources);
if (!$this->template instanceof \Twig_Template) {
$this->template = $this->environment->loadTemplate($this->template);
}
}
$custom = '_'.$view->getVar('id');
$rendering = $custom.$section;
$blocks = $this->getBlocks($view);
if (isset($this->varStack[$rendering])) {
$typeIndex = $this->varStack[$rendering]['typeIndex'] - 1;
$types = $this->varStack[$rendering]['types'];
$this->varStack[$rendering]['variables'] = array_replace_recursive($this->varStack[$rendering]['variables'], $variables);
} else {
$types = $view->getVar('types');
$types[] = $view->getVar('full_block_name');
$typeIndex = count($types) - 1;
$this->varStack[$rendering] = array(
'variables' => array_replace_recursive($view->getVars(), $variables),
'types' => $types,
);
}
do {
$types[$typeIndex] .= '_'.$section;
if (isset($blocks[$types[$typeIndex]])) {
$this->varStack[$rendering]['typeIndex'] = $typeIndex;
$context = $this->environment->mergeGlobals($this->varStack[$rendering]['variables']);
// we do not call renderBlock here to avoid too many nested level calls (XDebug limits the level to 100 by default)
ob_start();
$this->template->displayBlock($types[$typeIndex], $context, $blocks);
$html = ob_get_clean();
if ($mainTemplate) {
$view->setRendered();
}
unset($this->varStack[$rendering]);
return $html;
}
} while (--$typeIndex >= 0);
throw new FormException(sprintf(
'Unable to render the form as none of the following blocks exist: "%s".',
implode('", "', array_reverse($types))
));
}
/**
* Returns a CSRF token.
*
* Use this helper for CSRF protection without the overhead of creating a
* form.
*
* <code>
* <input type="hidden" name="token" value="{{ csrf_token('rm_user_' ~ user.id) }}">
* </code>
*
* Check the token in your action using the same intention.
*
* <code>
* $csrfProvider = $this->get('form.csrf_provider');
* if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) {
* throw new \RuntimeException('CSRF attack detected.');
* }
* </code>
*
* @param string $intention The intention of the protected action
*
* @return string A CSRF token
*/
public function getCsrfToken($intention)
{
if (!$this->csrfProvider instanceof CsrfProviderInterface) {
throw new \BadMethodCallException('CSRF token can only be generated if a CsrfProviderInterface is injected in the constructor.');
}
return $this->csrfProvider->generateCsrfToken($intention);
}
/**
* Returns the name of the extension.
*
* @return string The extension name
* {@inheritdoc}
*/
public function getName()
{
return 'form';
}
/**
* Returns the blocks used to render the view.
*
* Templates are looked for in the resources in the following order:
* * resources from the themes (and its parents)
* * resources from the themes of parent views (up to the root view)
* * default resources
*
* @param FormView $view The view
*
* @return array An array of Twig_TemplateInterface instances
*/
protected function getBlocks(FormView $view)
{
if (!$this->blocks->contains($view)) {
$rootView = !$view->hasParent();
$templates = $rootView ? $this->resources : array();
if (isset($this->themes[$view])) {
$templates = array_merge($templates, $this->themes[$view]);
}
$blocks = array();
foreach ($templates as $template) {
if (!$template instanceof \Twig_Template) {
$template = $this->environment->loadTemplate($template);
}
$templateBlocks = array();
do {
$templateBlocks = array_merge($template->getBlocks(), $templateBlocks);
} while (false !== $template = $template->getParent(array()));
$blocks = array_merge($blocks, $templateBlocks);
}
if (!$rootView) {
$blocks = array_merge($this->getBlocks($view->getParent()), $blocks);
}
$this->blocks->attach($view, $blocks);
} else {
$blocks = $this->blocks[$view];
}
return $blocks;
}
}
function humanize($text)
{
return ucfirst(trim(strtolower(preg_replace('/[_\s]+/', ' ', $text))));
}

View File

@ -0,0 +1,41 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Form;
use Symfony\Component\Form\FormRenderer;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TwigRenderer extends FormRenderer implements TwigRendererInterface
{
/**
* @var TwigRendererEngineInterface
*/
private $engine;
public function __construct(TwigRendererEngineInterface $engine, CsrfProviderInterface $csrfProvider = null)
{
parent::__construct($engine, $csrfProvider);
$this->engine = $engine;
}
/**
* {@inheritdoc}
*/
public function setEnvironment(\Twig_Environment $environment)
{
$this->engine->setEnvironment($environment);
}
}

View File

@ -0,0 +1,183 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Form;
use Symfony\Component\Form\AbstractRendererEngine;
use Symfony\Component\Form\FormViewInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TwigRendererEngine extends AbstractRendererEngine implements TwigRendererEngineInterface
{
/**
* @var \Twig_Environment
*/
private $environment;
/**
* @var \Twig_Template
*/
private $template;
/**
* {@inheritdoc}
*/
public function setEnvironment(\Twig_Environment $environment)
{
$this->environment = $environment;
}
/**
* {@inheritdoc}
*/
public function renderBlock(FormViewInterface $view, $resource, $block, array $variables = array())
{
$cacheKey = $view->getVar(self::CACHE_KEY_VAR);
$context = $this->environment->mergeGlobals($variables);
ob_start();
// By contract,This method can only be called after getting the resource
// (which is passed to the method). Getting a resource for the first time
// (with an empty cache) is guaranteed to invoke loadResourcesFromTheme(),
// where the property $template is initialized.
// We do not call renderBlock here to avoid too many nested level calls
// (XDebug limits the level to 100 by default)
$this->template->displayBlock($block, $context, $this->resources[$cacheKey]);
return ob_get_clean();
}
/**
* Loads the cache with the resource for a given block name.
*
* This implementation eagerly loads all blocks of the themes assigned to the given view
* and all of its ancestors views. This is necessary, because Twig receives the
* list of blocks later. At that point, all blocks must already be loaded, for the
* case that the function "block()" is used in the Twig template.
*
* @see getResourceForBlock()
*
* @param string $cacheKey The cache key of the form view.
* @param FormViewInterface $view The form view for finding the applying themes.
* @param string $block The name of the block to load.
*
* @return Boolean True if the resource could be loaded, false otherwise.
*/
protected function loadResourceForBlock($cacheKey, FormViewInterface $view, $block)
{
// The caller guarantees that $this->resources[$cacheKey][$block] is
// not set, but it doesn't have to check whether $this->resources[$cacheKey]
// is set. If $this->resources[$cacheKey] is set, all themes for this
// $cacheKey are already loaded (due to the eager population, see doc comment).
if (isset($this->resources[$cacheKey])) {
// As said in the previous, the caller guarantees that
// $this->resources[$cacheKey][$block] is not set. Since the themes are
// already loaded, it can only be a non-existing block.
$this->resources[$cacheKey][$block] = false;
return false;
}
// Recursively try to find the block in the themes assigned to $view,
// then of its parent view, then of the parent view of the parent and so on.
// When the root view is reached in this recursion, also the default
// themes are taken into account.
// Check each theme whether it contains the searched block
if (isset($this->themes[$cacheKey])) {
for ($i = count($this->themes[$cacheKey]) - 1; $i >= 0; --$i) {
$this->loadResourcesFromTheme($cacheKey, $this->themes[$cacheKey][$i]);
// CONTINUE LOADING (see doc comment)
}
}
// Check the default themes once we reach the root view without success
if (!$view->hasParent()) {
for ($i = count($this->defaultThemes) - 1; $i >= 0; --$i) {
$this->loadResourcesFromTheme($cacheKey, $this->defaultThemes[$i]);
// CONTINUE LOADING (see doc comment)
}
}
// Proceed with the themes of the parent view
if ($view->hasParent()) {
$parentCacheKey = $view->getParent()->getVar(self::CACHE_KEY_VAR);
if (!isset($this->resources[$parentCacheKey])) {
$this->loadResourceForBlock($parentCacheKey, $view->getParent(), $block);
}
// EAGER CACHE POPULATION (see doc comment)
foreach ($this->resources[$parentCacheKey] as $blockName => $resource) {
if (!isset($this->resources[$cacheKey][$blockName])) {
$this->resources[$cacheKey][$blockName] = $resource;
}
}
}
// Even though we loaded the themes, it can happen that none of them
// contains the searched block
if (!isset($this->resources[$cacheKey][$block])) {
// Cache that we didn't find anything to speed up further accesses
$this->resources[$cacheKey][$block] = false;
}
return false !== $this->resources[$cacheKey][$block];
}
/**
* Loads the resources for all blocks in a theme.
*
* @param string $cacheKey The cache key for storing the resource.
* @param mixed $theme The theme to load the block from. This parameter
* is passed by reference, because it might be necessary
* to initialize the theme first. Any changes made to
* this variable will be kept and be available upon
* further calls to this method using the same theme.
*/
protected function loadResourcesFromTheme($cacheKey, &$theme)
{
if (!$theme instanceof \Twig_Template) {
/* @var \Twig_Template $theme */
$theme = $this->environment->loadTemplate($theme);
}
if (null === $this->template) {
// Store the first \Twig_Template instance that we find so that
// we can call displayBlock() later on. It doesn't matter *which*
// template we use for that, since we pass the used blocks manually
// anyway.
$this->template = $theme;
}
// Use a separate variable for the inheritance traversal, because
// theme is a reference and we don't want to change it.
$currentTheme = $theme;
// The do loop takes care of template inheritance.
// Add blocks from all templates in the inheritance tree, but avoid
// overriding blocks already set.
do {
foreach ($currentTheme->getBlocks() as $block => $blockData) {
if (!isset($this->resources[$cacheKey][$block])) {
// The resource given back is the key to the bucket that
// contains this block.
$this->resources[$cacheKey][$block] = $blockData;
}
}
} while (false !== $currentTheme = $currentTheme->getParent(array()));
}
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Form;
use Symfony\Component\Form\FormRendererEngineInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface TwigRendererEngineInterface extends FormRendererEngineInterface
{
/**
* Sets Twig's environment.
*
* @param \Twig_Environment $environment
*/
public function setEnvironment(\Twig_Environment $environment);
}

View File

@ -0,0 +1,27 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Twig\Form;
use Symfony\Component\Form\FormRendererInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface TwigRendererInterface extends FormRendererInterface
{
/**
* Sets Twig's environment.
*
* @param \Twig_Environment $environment
*/
public function setEnvironment(\Twig_Environment $environment);
}

View File

@ -30,7 +30,7 @@ class FormThemeNode extends \Twig_Node
{
$compiler
->addDebugInfo($this)
->write('echo $this->env->getExtension(\'form\')->setTheme(')
->write('$this->env->getExtension(\'form\')->renderer->setTheme(')
->subcompile($this->getNode('form'))
->raw(', ')
->subcompile($this->getNode('resources'))

View File

@ -12,6 +12,8 @@
namespace Symfony\Bridge\Twig\Tests\Extension;
use Symfony\Bridge\Twig\Extension\FormExtension;
use Symfony\Bridge\Twig\Form\TwigRenderer;
use Symfony\Bridge\Twig\Form\TwigRendererEngine;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator;
use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader;
@ -20,6 +22,9 @@ use Symfony\Component\Form\Tests\AbstractDivLayoutTest;
class FormExtensionDivLayoutTest extends AbstractDivLayoutTest
{
/**
* @var FormExtension
*/
protected $extension;
protected function setUp()
@ -42,20 +47,23 @@ class FormExtensionDivLayoutTest extends AbstractDivLayoutTest
parent::setUp();
$loader = new StubFilesystemLoader(array(
__DIR__.'/../../../../../../src/Symfony/Bridge/Twig/Resources/views/Form',
__DIR__,
));
$this->extension = new FormExtension($this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array(
$rendererEngine = new TwigRendererEngine(array(
'form_div_layout.html.twig',
'custom_widgets.html.twig',
));
$renderer = new TwigRenderer($rendererEngine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'));
$this->extension = new FormExtension($renderer);
$loader = new StubFilesystemLoader(array(
__DIR__.'/../../Resources/views/Form',
__DIR__,
));
$environment = new \Twig_Environment($loader, array('strict_variables' => true));
$environment->addExtension($this->extension);
$environment->addExtension(new TranslationExtension(new StubTranslator()));
$environment->addGlobal('global', '');
$environment->addExtension($this->extension);
$this->extension->initRuntime($environment);
}
@ -99,37 +107,37 @@ class FormExtensionDivLayoutTest extends AbstractDivLayoutTest
protected function renderEnctype(FormView $view)
{
return (string) $this->extension->renderEnctype($view);
return (string) $this->extension->renderer->renderEnctype($view);
}
protected function renderLabel(FormView $view, $label = null, array $vars = array())
{
return (string) $this->extension->renderLabel($view, $label, $vars);
return (string) $this->extension->renderer->renderLabel($view, $label, $vars);
}
protected function renderErrors(FormView $view)
{
return (string) $this->extension->renderErrors($view);
return (string) $this->extension->renderer->renderErrors($view);
}
protected function renderWidget(FormView $view, array $vars = array())
{
return (string) $this->extension->renderWidget($view, $vars);
return (string) $this->extension->renderer->renderWidget($view, $vars);
}
protected function renderRow(FormView $view, array $vars = array())
{
return (string) $this->extension->renderRow($view, $vars);
return (string) $this->extension->renderer->renderRow($view, $vars);
}
protected function renderRest(FormView $view, array $vars = array())
{
return (string) $this->extension->renderRest($view, $vars);
return (string) $this->extension->renderer->renderRest($view, $vars);
}
protected function setTheme(FormView $view, array $themes)
{
$this->extension->setTheme($view, $themes);
$this->extension->renderer->setTheme($view, $themes);
}
public static function themeBlockInheritanceProvider()

View File

@ -12,6 +12,8 @@
namespace Symfony\Bridge\Twig\Tests\Extension;
use Symfony\Component\Form\FormView;
use Symfony\Bridge\Twig\Form\TwigRenderer;
use Symfony\Bridge\Twig\Form\TwigRendererEngine;
use Symfony\Bridge\Twig\Extension\FormExtension;
use Symfony\Bridge\Twig\Extension\TranslationExtension;
use Symfony\Component\Form\Tests\AbstractTableLayoutTest;
@ -20,6 +22,9 @@ use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubFilesystemLoader;
class FormExtensionTableLayoutTest extends AbstractTableLayoutTest
{
/**
* @var FormExtension
*/
protected $extension;
protected function setUp()
@ -42,20 +47,23 @@ class FormExtensionTableLayoutTest extends AbstractTableLayoutTest
parent::setUp();
$loader = new StubFilesystemLoader(array(
__DIR__.'/../../../../../../src/Symfony/Bridge/Twig/Resources/views/Form',
__DIR__,
));
$this->extension = new FormExtension($this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array(
$rendererEngine = new TwigRendererEngine(array(
'form_table_layout.html.twig',
'custom_widgets.html.twig',
));
$renderer = new TwigRenderer($rendererEngine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'));
$this->extension = new FormExtension($renderer);
$loader = new StubFilesystemLoader(array(
__DIR__.'/../../Resources/views/Form',
__DIR__,
));
$environment = new \Twig_Environment($loader, array('strict_variables' => true));
$environment->addExtension($this->extension);
$environment->addExtension(new TranslationExtension(new StubTranslator()));
$environment->addGlobal('global', '');
$environment->addExtension($this->extension);
$this->extension->initRuntime($environment);
}
@ -69,36 +77,36 @@ class FormExtensionTableLayoutTest extends AbstractTableLayoutTest
protected function renderEnctype(FormView $view)
{
return (string) $this->extension->renderEnctype($view);
return (string) $this->extension->renderer->renderEnctype($view);
}
protected function renderLabel(FormView $view, $label = null, array $vars = array())
{
return (string) $this->extension->renderLabel($view, $label, $vars);
return (string) $this->extension->renderer->renderLabel($view, $label, $vars);
}
protected function renderErrors(FormView $view)
{
return (string) $this->extension->renderErrors($view);
return (string) $this->extension->renderer->renderErrors($view);
}
protected function renderWidget(FormView $view, array $vars = array())
{
return (string) $this->extension->renderWidget($view, $vars);
return (string) $this->extension->renderer->renderWidget($view, $vars);
}
protected function renderRow(FormView $view, array $vars = array())
{
return (string) $this->extension->renderRow($view, $vars);
return (string) $this->extension->renderer->renderRow($view, $vars);
}
protected function renderRest(FormView $view, array $vars = array())
{
return (string) $this->extension->renderRest($view, $vars);
return (string) $this->extension->renderer->renderRest($view, $vars);
}
protected function setTheme(FormView $view, array $themes)
{
$this->extension->setTheme($view, $themes);
$this->extension->renderer->setTheme($view, $themes);
}
}

View File

@ -55,7 +55,7 @@ class FormThemeTest extends TestCase
$this->assertEquals(
sprintf(
'echo $this->env->getExtension(\'form\')->setTheme(%s, array(0 => "tpl1", 1 => "tpl2"));',
'$this->env->getExtension(\'form\')->renderer->setTheme(%s, array(0 => "tpl1", 1 => "tpl2"));',
$this->getVariableGetter('form')
),
trim($compiler->compile($node)->getSource())
@ -67,7 +67,7 @@ class FormThemeTest extends TestCase
$this->assertEquals(
sprintf(
'echo $this->env->getExtension(\'form\')->setTheme(%s, "tpl1");',
'$this->env->getExtension(\'form\')->renderer->setTheme(%s, "tpl1");',
$this->getVariableGetter('form')
),
trim($compiler->compile($node)->getSource())

View File

@ -1,89 +0,0 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\FrameworkBundle\DataCollector;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector as BaseRequestDataCollector;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
/**
* RequestDataCollector.
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class RequestDataCollector extends BaseRequestDataCollector
{
protected $controllers;
public function __construct()
{
$this->controllers = new \SplObjectStorage();
}
/**
* {@inheritdoc}
*/
public function collect(Request $request, Response $response, \Exception $exception = null)
{
parent::collect($request, $response, $exception);
$this->data['controller'] = 'n/a';
if (isset($this->controllers[$request])) {
$controller = $this->controllers[$request];
if (is_array($controller)) {
$r = new \ReflectionMethod($controller[0], $controller[1]);
$this->data['controller'] = array(
'class' => get_class($controller[0]),
'method' => $controller[1],
'file' => $r->getFilename(),
'line' => $r->getStartLine(),
);
} elseif ($controller instanceof \Closure) {
$this->data['controller'] = 'Closure';
} else {
$this->data['controller'] = (string) $controller ?: 'n/a';
}
unset($this->controllers[$request]);
}
}
public function onKernelController(FilterControllerEvent $event)
{
$this->controllers[$event->getRequest()] = $event->getController();
}
/**
* {@inheritdoc}
*/
public function getRoute()
{
return isset($this->data['request_attributes']['_route']) ? $this->data['request_attributes']['_route'] : '';
}
/**
* {@inheritdoc}
*/
public function getRouteParams()
{
return isset($this->data['request_attributes']['_route_params']) ? $this->data['request_attributes']['_route_params'] : array();
}
/**
* {@inheritdoc}
*/
public function getController()
{
return $this->data['controller'];
}
}

View File

@ -6,7 +6,7 @@
<parameters>
<parameter key="data_collector.config.class">Symfony\Component\HttpKernel\DataCollector\ConfigDataCollector</parameter>
<parameter key="data_collector.request.class">Symfony\Bundle\FrameworkBundle\DataCollector\RequestDataCollector</parameter>
<parameter key="data_collector.request.class">Symfony\Component\HttpKernel\DataCollector\RequestDataCollector</parameter>
<parameter key="data_collector.exception.class">Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector</parameter>
<parameter key="data_collector.events.class">Symfony\Component\HttpKernel\DataCollector\EventDataCollector</parameter>
<parameter key="data_collector.logger.class">Symfony\Component\HttpKernel\DataCollector\LoggerDataCollector</parameter>
@ -22,7 +22,7 @@
</service>
<service id="data_collector.request" class="%data_collector.request.class%">
<tag name="kernel.event_listener" event="kernel.controller" method="onKernelController"/>
<tag name="kernel.event_subscriber" />
<tag name="data_collector" template="WebProfilerBundle:Collector:request" id="request" priority="255" />
</service>

View File

@ -15,6 +15,8 @@
<parameter key="templating.helper.code.class">Symfony\Bundle\FrameworkBundle\Templating\Helper\CodeHelper</parameter>
<parameter key="templating.helper.translator.class">Symfony\Bundle\FrameworkBundle\Templating\Helper\TranslatorHelper</parameter>
<parameter key="templating.helper.form.class">Symfony\Bundle\FrameworkBundle\Templating\Helper\FormHelper</parameter>
<parameter key="templating.form.engine.class">Symfony\Component\Form\Extension\Templating\TemplatingRendererEngine</parameter>
<parameter key="templating.form.renderer.class">Symfony\Component\Form\FormRenderer</parameter>
<parameter key="templating.globals.class">Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables</parameter>
<parameter key="templating.asset.path_package.class">Symfony\Bundle\FrameworkBundle\Templating\Asset\PathPackage</parameter>
<parameter key="templating.asset.url_package.class">Symfony\Component\Templating\Asset\UrlPackage</parameter>
@ -96,11 +98,19 @@
<service id="templating.helper.form" class="%templating.helper.form.class%">
<tag name="templating.helper" alias="form" />
<argument type="service" id="templating.form.renderer" />
</service>
<service id="templating.form.engine" class="%templating.form.engine.class%" public="false">
<argument type="service" id="templating.engine.php" />
<argument type="service" id="form.csrf_provider" on-invalid="null" />
<argument>%templating.helper.form.resources%</argument>
</service>
<service id="templating.form.renderer" class="%templating.form.renderer.class%" public="false">
<argument type="service" id="templating.form.engine" />
<argument type="service" id="form.csrf_provider" on-invalid="null" />
</service>
<service id="templating.globals" class="%templating.globals.class%">
<argument type="service" id="service_container" />
</service>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('widget_attributes') ?>
<?php echo $view['form']->block('widget_attributes') ?>

View File

@ -1,5 +1,5 @@
<input type="checkbox"
<?php echo $view['form']->renderBlock('widget_attributes') ?>
<?php echo $view['form']->block('widget_attributes') ?>
<?php if ($value): ?> value="<?php echo $view->escape($value) ?>"<?php endif ?>
<?php if ($checked): ?> checked="checked"<?php endif ?>
/>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('choice_widget_options') ?>
<?php echo $view['form']->block('choice_widget_options') ?>

View File

@ -1,5 +1,5 @@
<?php if ($expanded): ?>
<?php echo $view['form']->renderBlock('choice_widget_expanded') ?>
<?php echo $view['form']->block('choice_widget_expanded') ?>
<?php else: ?>
<?php echo $view['form']->renderBlock('choice_widget_collapsed') ?>
<?php echo $view['form']->block('choice_widget_collapsed') ?>
<?php endif ?>

View File

@ -1,13 +1,13 @@
<select
<?php echo $view['form']->renderBlock('widget_attributes') ?>
<?php echo $view['form']->block('widget_attributes') ?>
<?php if ($multiple): ?> multiple="multiple"<?php endif ?>
>
<?php if (null !== $empty_value): ?><option value=""><?php echo $view->escape($view['translator']->trans($empty_value, array(), $translation_domain)) ?></option><?php endif; ?>
<?php if (count($preferred_choices) > 0): ?>
<?php echo $view['form']->renderBlock('choice_widget_options', array('options' => $preferred_choices)) ?>
<?php echo $view['form']->block('choice_widget_options', array('options' => $preferred_choices)) ?>
<?php if (count($choices) > 0 && null !== $separator): ?>
<option disabled="disabled"><?php echo $separator ?></option>
<?php endif ?>
<?php endif ?>
<?php echo $view['form']->renderBlock('choice_widget_options', array('options' => $choices)) ?>
<?php echo $view['form']->block('choice_widget_options', array('options' => $choices)) ?>
</select>

View File

@ -1,4 +1,4 @@
<div <?php echo $view['form']->renderBlock('widget_container_attributes') ?>>
<div <?php echo $view['form']->block('widget_container_attributes') ?>>
<?php foreach ($form as $child): ?>
<?php echo $view['form']->widget($child) ?>
<?php echo $view['form']->label($child) ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('widget_container_attributes') ?>
<?php echo $view['form']->block('widget_container_attributes') ?>

View File

@ -1,7 +1,7 @@
<?php if ($widget == 'single_text'): ?>
<?php echo $view['form']->renderBlock('form_widget_simple'); ?>
<?php echo $view['form']->block('form_widget_simple'); ?>
<?php else: ?>
<div <?php echo $view['form']->renderBlock('widget_container_attributes') ?>>
<div <?php echo $view['form']->block('widget_container_attributes') ?>>
<?php echo str_replace(array('{{ year }}', '{{ month }}', '{{ day }}'), array(
$view['form']->widget($form['year']),
$view['form']->widget($form['month']),

View File

@ -1,7 +1,7 @@
<?php if ($widget == 'single_text'): ?>
<?php echo $view['form']->renderBlock('form_widget_simple'); ?>
<?php echo $view['form']->block('form_widget_simple'); ?>
<?php else: ?>
<div <?php echo $view['form']->renderBlock('widget_container_attributes') ?>>
<div <?php echo $view['form']->block('widget_container_attributes') ?>>
<?php echo $view['form']->widget($form['date']).' '.$view['form']->widget($form['time']) ?>
</div>
<?php endif ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : 'email')) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : 'email')) ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_enctype') ?>
<?php echo $view['form']->block('form_enctype') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_errors') ?>
<?php echo $view['form']->block('form_errors') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_label') ?>
<?php echo $view['form']->block('form_label') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_rest') ?>
<?php echo $view['form']->block('form_rest') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_row') ?>
<?php echo $view['form']->block('form_row') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_rows') ?>
<?php echo $view['form']->block('form_rows') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple') ?>
<?php echo $view['form']->block('form_widget_simple') ?>

View File

@ -1,5 +1,5 @@
<?php if ($compound): ?>
<?php echo $view['form']->renderBlock('form_widget_compound')?>
<?php echo $view['form']->block('form_widget_compound')?>
<?php else: ?>
<?php echo $view['form']->renderBlock('form_widget_simple')?>
<?php echo $view['form']->block('form_widget_simple')?>
<?php endif ?>

View File

@ -1,4 +1,4 @@
<div <?php echo $view['form']->renderBlock('widget_container_attributes') ?>>
<div <?php echo $view['form']->block('widget_container_attributes') ?>>
<?php if (!$form->hasParent() && $errors): ?>
<tr>
<td colspan="2">
@ -6,6 +6,6 @@
</td>
</tr>
<?php endif ?>
<?php echo $view['form']->renderBlock('form_rows') ?>
<?php echo $view['form']->block('form_rows') ?>
<?php echo $view['form']->rest($form) ?>
</div>

View File

@ -1,5 +1,5 @@
<input
type="<?php echo isset($type) ? $view->escape($type) : 'text' ?>"
<?php if (!empty($value)): ?>value="<?php echo $view->escape($value) ?>"<?php endif ?>
<?php echo $view['form']->renderBlock('widget_attributes') ?>
<?php echo $view['form']->block('widget_attributes') ?>
/>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "hidden")) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "hidden")) ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "number")) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "number")) ?>

View File

@ -1 +1 @@
<?php echo str_replace('{{ widget }}', $view['form']->renderBlock('form_widget_simple'), $money_pattern) ?>
<?php echo str_replace('{{ widget }}', $view['form']->block('form_widget_simple'), $money_pattern) ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "text")) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "text")) ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "password")) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "password")) ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "text")) ?> %
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "text")) ?> %

View File

@ -1,5 +1,5 @@
<input type="radio"
<?php echo $view['form']->renderBlock('widget_attributes') ?>
<?php echo $view['form']->block('widget_attributes') ?>
value="<?php echo $view->escape($value) ?>"
<?php if ($checked): ?> checked="checked"<?php endif ?>
/>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_rows') ?>
<?php echo $view['form']->block('form_rows') ?>

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "search")) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "search")) ?>

View File

@ -1 +1 @@
<textarea <?php echo $view['form']->renderBlock('widget_attributes') ?>><?php echo $view->escape($value) ?></textarea>
<textarea <?php echo $view['form']->block('widget_attributes') ?>><?php echo $view->escape($value) ?></textarea>

View File

@ -1,7 +1,7 @@
<?php if ($widget == 'single_text'): ?>
<?php echo $view['form']->renderBlock('form_widget_simple'); ?>
<?php echo $view['form']->block('form_widget_simple'); ?>
<?php else: ?>
<div <?php echo $view['form']->renderBlock('widget_container_attributes') ?>>
<div <?php echo $view['form']->block('widget_container_attributes') ?>>
<?php
// There should be no spaces between the colons and the widgets, that's why
// this block is written in a single PHP tag

View File

@ -1 +1 @@
<?php echo $view['form']->renderBlock('form_widget_simple', array('type' => isset($type) ? $type : "url")) ?>
<?php echo $view['form']->block('form_widget_simple', array('type' => isset($type) ? $type : "url")) ?>

View File

@ -1,7 +1,7 @@
<table <?php echo $view['form']->renderBlock('widget_container_attributes') ?>>
<table <?php echo $view['form']->block('widget_container_attributes') ?>>
<?php if (!$form->hasParent()): ?>
<?php echo $view['form']->errors($form) ?>
<?php endif ?>
<?php echo $view['form']->renderBlock('form_rows') ?>
<?php echo $view['form']->block('form_rows') ?>
<?php echo $view['form']->rest($form) ?>
</table>

View File

@ -12,8 +12,9 @@
namespace Symfony\Bundle\FrameworkBundle\Templating\Helper;
use Symfony\Component\Templating\Helper\Helper;
use Symfony\Component\Form\FormRendererInterface;
use Symfony\Component\Form\FormViewInterface;
use Symfony\Component\Templating\EngineInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
@ -27,46 +28,35 @@ use Symfony\Component\Form\Util\FormUtil;
*/
class FormHelper extends Helper
{
protected $engine;
protected $csrfProvider;
protected $varStack;
protected $context;
protected $resources;
protected $themes;
protected $templates;
/**
* @var FormRendererInterface
*/
private $renderer;
/**
* Constructor.
*
* @param EngineInterface $engine The templating engine
* @param CsrfProviderInterface $csrfProvider The CSRF provider
* @param array $resources An array of theme names
* @param FormRendererInterface $renderer
*/
public function __construct(EngineInterface $engine, CsrfProviderInterface $csrfProvider = null, array $resources = array())
public function __construct(FormRendererInterface $renderer)
{
$this->engine = $engine;
$this->csrfProvider = $csrfProvider;
$this->resources = $resources;
$this->varStack = array();
$this->context = array();
$this->templates = array();
$this->themes = array();
$this->renderer = $renderer;
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'form';
}
public function isChoiceGroup($label)
{
return FormUtil::isChoiceGroup($label);
return $this->renderer->isChoiceGroup($label);
}
public function isChoiceSelected(FormView $view, ChoiceView $choice)
public function isChoiceSelected(FormViewInterface $view, ChoiceView $choice)
{
return FormUtil::isChoiceSelected($choice->getValue(), $view->getVar('value'));
return $this->renderer->isChoiceSelected($view, $choice);
}
/**
@ -74,13 +64,12 @@ class FormHelper extends Helper
*
* The theme format is "<Bundle>:<Controller>".
*
* @param FormView $view A FormView instance
* @param FormViewInterface $view A FormViewInterface instance
* @param string|array $themes A theme or an array of theme
*/
public function setTheme(FormView $view, $themes)
public function setTheme(FormViewInterface $view, $themes)
{
$this->themes[$view->getVar('id')] = (array) $themes;
$this->templates = array();
$this->renderer->setTheme($view, $themes);
}
/**
@ -90,13 +79,13 @@ class FormHelper extends Helper
*
* <form action="..." method="post" <?php echo $view['form']->enctype() ?>>
*
* @param FormView $view The view for which to render the encoding type
* @param FormViewInterface $view The view for which to render the encoding type
*
* @return string The html markup
* @return string The HTML markup
*/
public function enctype(FormView $view)
public function enctype(FormViewInterface $view)
{
return $this->renderSection($view, 'enctype');
return $this->renderer->renderEnctype($view);
}
/**
@ -112,70 +101,95 @@ class FormHelper extends Helper
*
* <?php echo view['form']->widget(array('separator' => '+++++)) ?>
*
* @param FormView $view The view for which to render the widget
* @param FormViewInterface $view The view for which to render the widget
* @param array $variables Additional variables passed to the template
*
* @return string The html markup
* @return string The HTML markup
*/
public function widget(FormView $view, array $variables = array())
public function widget(FormViewInterface $view, array $variables = array())
{
return $this->renderSection($view, 'widget', $variables);
return $this->renderer->renderWidget($view, $variables);
}
/**
* Renders the entire form field "row".
*
* @param FormView $view The view for which to render the row
* @param FormViewInterface $view The view for which to render the row
* @param array $variables Additional variables passed to the template
*
* @return string The html markup
* @return string The HTML markup
*/
public function row(FormView $view, array $variables = array())
public function row(FormViewInterface $view, array $variables = array())
{
return $this->renderSection($view, 'row', $variables);
return $this->renderer->renderRow($view, $variables);
}
/**
* Renders the label of the given view.
*
* @param FormView $view The view for which to render the label
* @param FormViewInterface $view The view for which to render the label
* @param string $label The label
* @param array $variables Additional variables passed to the template
*
* @return string The html markup
* @return string The HTML markup
*/
public function label(FormView $view, $label = null, array $variables = array())
public function label(FormViewInterface $view, $label = null, array $variables = array())
{
if ($label !== null) {
$variables += array('label' => $label);
}
return $this->renderSection($view, 'label', $variables);
return $this->renderer->renderLabel($view, $label, $variables);
}
/**
* Renders the errors of the given view.
*
* @param FormView $view The view to render the errors for
* @param FormViewInterface $view The view to render the errors for
*
* @return string The html markup
* @return string The HTML markup
*/
public function errors(FormView $view)
public function errors(FormViewInterface $view)
{
return $this->renderSection($view, 'errors');
return $this->renderer->renderErrors($view);
}
/**
* Renders views which have not already been rendered.
*
* @param FormView $view The parent view
* @param FormViewInterface $view The parent view
* @param array $variables An array of variables
*
* @return string The html markup
* @return string The HTML markup
*/
public function rest(FormView $view, array $variables = array())
public function rest(FormViewInterface $view, array $variables = array())
{
return $this->renderSection($view, 'rest', $variables);
return $this->renderer->renderRest($view, $variables);
}
/**
* Alias of {@link block()}
*
* @param string $block The name of the block to render.
* @param array $variables The variable to pass to the template.
*
* @return string The HTML markup
*
* @deprecated Deprecated since version 2.1, to be removed in 2.3. Use
* {@link block()} instead.
*/
public function renderBlock($block, array $variables = array())
{
return $this->block($block, $variables);
}
/**
* Renders a block of the template.
*
* @param string $block The name of the block to render.
* @param array $variables The variable to pass to the template.
*
* @return string The HTML markup
*/
public function block($block, array $variables = array())
{
return $this->renderer->renderBlock($block, $variables);
}
/**
@ -200,166 +214,16 @@ class FormHelper extends Helper
* @param string $intention The intention of the protected action
*
* @return string A CSRF token
*
* @throws \BadMethodCallException When no CSRF provider was injected in the constructor.
*/
public function csrfToken($intention)
{
if (!$this->csrfProvider instanceof CsrfProviderInterface) {
throw new \BadMethodCallException('CSRF token can only be generated if a CsrfProviderInterface is injected in the constructor.');
}
return $this->csrfProvider->generateCsrfToken($intention);
}
/**
* Renders a template.
*
* 1. This function first looks for a block named "_<view id>_<section>",
* 2. if such a block is not found the function will look for a block named
* "<type name>_<section>",
* 3. the type name is recursively replaced by the parent type name until a
* corresponding block is found
*
* @param FormView $view The form view
* @param string $section The section to render (i.e. 'row', 'widget', 'label', ...)
* @param array $variables Additional variables
*
* @return string The html markup
*
* @throws FormException if no template block exists to render the given section of the view
*/
protected function renderSection(FormView $view, $section, array $variables = array())
{
$mainTemplate = in_array($section, array('row', 'widget'));
if ($mainTemplate && $view->isRendered()) {
return '';
}
$template = null;
$custom = '_'.$view->getVar('id');
$rendering = $custom.$section;
if (isset($this->varStack[$rendering])) {
$typeIndex = $this->varStack[$rendering]['typeIndex'] - 1;
$types = $this->varStack[$rendering]['types'];
$variables = array_replace_recursive($this->varStack[$rendering]['variables'], $variables);
} else {
$types = $view->getVar('types');
$types[] = $view->getVar('full_block_name');
$typeIndex = count($types) - 1;
$variables = array_replace_recursive($view->getVars(), $variables);
$this->varStack[$rendering]['types'] = $types;
}
$this->varStack[$rendering]['variables'] = $variables;
do {
$types[$typeIndex] .= '_'.$section;
$template = $this->lookupTemplate($view, $types[$typeIndex]);
if ($template) {
$this->varStack[$rendering]['typeIndex'] = $typeIndex;
$this->context[] = array(
'variables' => $variables,
'view' => $view,
);
$html = $this->engine->render($template, $variables);
array_pop($this->context);
unset($this->varStack[$rendering]);
if ($mainTemplate) {
$view->setRendered();
}
return trim($html);
}
} while (--$typeIndex >= 0);
throw new FormException(sprintf(
'Unable to render the form as none of the following blocks exist: "%s".',
implode('", "', array_reverse($types))
));
}
/**
* Render a block from a form element.
*
* @param string $name
* @param array $variables Additional variables (those would override the current context)
*
* @throws FormException if the block is not found
* @throws FormException if the method is called out of a form element (no context)
*/
public function renderBlock($name, $variables = array())
{
if (0 == count($this->context)) {
throw new FormException(sprintf('This method should only be called while rendering a form element.', $name));
}
$context = end($this->context);
$template = $this->lookupTemplate($context['view'], $name);
if (false === $template) {
throw new FormException(sprintf('No block "%s" found while rendering the form.', $name));
}
$variables = array_replace_recursive($context['variables'], $variables);
return trim($this->engine->render($template, $variables));
return $this->renderer->renderCsrfToken($intention);
}
public function humanize($text)
{
return ucfirst(trim(strtolower(preg_replace('/[_\s]+/', ' ', $text))));
}
public function getName()
{
return 'form';
}
/**
* Returns the name of the template to use to render the block
*
* @param FormView $view The form view
* @param string $block The name of the block
*
* @return string|Boolean The template logical name or false when no template is found
*/
protected function lookupTemplate(FormView $view, $block)
{
$file = $block.'.html.php';
$id = $view->getVar('id');
if (!isset($this->templates[$id][$block])) {
$template = false;
$themes = $view->hasParent() ? array() : $this->resources;
if (isset($this->themes[$id])) {
$themes = array_merge($themes, $this->themes[$id]);
}
for ($i = count($themes) - 1; $i >= 0; --$i) {
if ($this->engine->exists($templateName = $themes[$i].':'.$file)) {
$template = $templateName;
break;
}
}
if (false === $template && $view->hasParent()) {
$template = $this->lookupTemplate($view->getParent(), $block);
}
$this->templates[$id][$block] = $template;
}
return $this->templates[$id][$block];
return $this->renderer->humanize($text);
}
}

View File

@ -15,9 +15,11 @@ use Symfony\Bundle\FrameworkBundle\Templating\Helper\FormHelper;
use Symfony\Bundle\FrameworkBundle\Templating\Helper\TranslatorHelper;
use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTemplateNameParser;
use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTranslator;
use Symfony\Component\Form\FormView;
use Symfony\Component\Templating\PhpEngine;
use Symfony\Component\Templating\Loader\FilesystemLoader;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormRenderer;
use Symfony\Component\Form\Extension\Templating\TemplatingRendererEngine;
use Symfony\Component\Form\Tests\AbstractDivLayoutTest;
class FormHelperDivLayoutTest extends AbstractDivLayoutTest
@ -26,6 +28,8 @@ class FormHelperDivLayoutTest extends AbstractDivLayoutTest
protected function setUp()
{
ini_set('xdebug.max_nesting_level', 120);
parent::setUp();
$root = realpath(__DIR__.'/../../../Resources/views');
@ -34,8 +38,10 @@ class FormHelperDivLayoutTest extends AbstractDivLayoutTest
$loader = new FilesystemLoader(array());
$engine = new PhpEngine($templateNameParser, $loader);
$engine->addGlobal('global', '');
$rendererEngine = new TemplatingRendererEngine($engine, array('FrameworkBundle:Form'));
$renderer = new FormRenderer($rendererEngine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'));
$this->helper = new FormHelper($engine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array('FrameworkBundle:Form'));
$this->helper = new FormHelper($renderer);
$engine->setHelpers(array(
$this->helper,

View File

@ -16,9 +16,11 @@ use Symfony\Bundle\FrameworkBundle\Templating\Helper\TranslatorHelper;
use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTemplateNameParser;
use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTranslator;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\FormRenderer;
use Symfony\Component\Form\Extension\Templating\TemplatingRendererEngine;
use Symfony\Component\Form\Tests\AbstractTableLayoutTest;
use Symfony\Component\Templating\PhpEngine;
use Symfony\Component\Templating\Loader\FilesystemLoader;
use Symfony\Component\Form\Tests\AbstractTableLayoutTest;
class FormHelperTableLayoutTest extends AbstractTableLayoutTest
{
@ -34,11 +36,13 @@ class FormHelperTableLayoutTest extends AbstractTableLayoutTest
$loader = new FilesystemLoader(array());
$engine = new PhpEngine($templateNameParser, $loader);
$engine->addGlobal('global', '');
$this->helper = new FormHelper($engine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'), array(
$rendererEngine = new TemplatingRendererEngine($engine, array(
'FrameworkBundle:Form',
'FrameworkBundle:FormTable'
));
$renderer = new FormRenderer($rendererEngine, $this->getMock('Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface'));
$this->helper = new FormHelper($renderer);
$engine->setHelpers(array(
$this->helper,

View File

@ -172,7 +172,7 @@ abstract class AbstractFactory implements SecurityFactoryInterface
return $config['success_handler'];
}
$successHandlerId = 'security.authentication.success_handler.'.$id;
$successHandlerId = 'security.authentication.success_handler.'.$id.'.'.str_replace('-', '_', $this->getKey());
$successHandler = $container->setDefinition($successHandlerId, new DefinitionDecorator('security.authentication.success_handler'));
$successHandler->replaceArgument(1, array_intersect_key($config, $this->defaultSuccessHandlerOptions));
@ -187,7 +187,7 @@ abstract class AbstractFactory implements SecurityFactoryInterface
return $config['failure_handler'];
}
$id = 'security.authentication.failure_handler.'.$id;
$id = 'security.authentication.failure_handler.'.$id.'.'.str_replace('-', '_', $this->getKey());
$failureHandler = $container->setDefinition($id, new DefinitionDecorator('security.authentication.failure_handler'));
$failureHandler->replaceArgument(2, array_intersect_key($config, $this->defaultFailureHandlerOptions));

View File

@ -54,7 +54,7 @@ class AbstractFactoryTest extends \PHPUnit_Framework_TestCase
$definition = $container->getDefinition('abstract_listener.foo');
$arguments = $definition->getArguments();
$this->assertEquals(new Reference('security.authentication.failure_handler.foo'), $arguments['index_6']);
$this->assertEquals(new Reference('security.authentication.failure_handler.foo.abstract_factory'), $arguments['index_6']);
}
public function testDefaultSuccessHandler()
@ -67,7 +67,7 @@ class AbstractFactoryTest extends \PHPUnit_Framework_TestCase
$definition = $container->getDefinition('abstract_listener.foo');
$arguments = $definition->getArguments();
$this->assertEquals(new Reference('security.authentication.success_handler.foo'), $arguments['index_5']);
$this->assertEquals(new Reference('security.authentication.success_handler.foo.abstract_factory'), $arguments['index_5']);
}
protected function callFactory($id, $config, $userProviderId, $defaultEntryPointId)
@ -84,6 +84,11 @@ class AbstractFactoryTest extends \PHPUnit_Framework_TestCase
->method('getListenerId')
->will($this->returnValue('abstract_listener'))
;
$factory
->expects($this->any())
->method('getKey')
->will($this->returnValue('abstract_factory'))
;
$container = new ContainerBuilder();
$container->register('auth_provider');

View File

@ -16,6 +16,8 @@
<parameter key="twig.extension.routing.class">Symfony\Bridge\Twig\Extension\RoutingExtension</parameter>
<parameter key="twig.extension.yaml.class">Symfony\Bridge\Twig\Extension\YamlExtension</parameter>
<parameter key="twig.extension.form.class">Symfony\Bridge\Twig\Extension\FormExtension</parameter>
<parameter key="twig.form.engine.class">Symfony\Bridge\Twig\Form\TwigRendererEngine</parameter>
<parameter key="twig.form.renderer.class">Symfony\Bridge\Twig\Form\TwigRenderer</parameter>
<parameter key="twig.translation.extractor.class">Symfony\Bridge\Twig\Translation\TwigExtractor</parameter>
<parameter key="twig.exception_listener.class">Symfony\Component\HttpKernel\EventListener\ExceptionListener</parameter>
</parameters>
@ -75,10 +77,18 @@
<service id="twig.extension.form" class="%twig.extension.form.class%" public="false">
<tag name="twig.extension" />
<argument type="service" id="form.csrf_provider" on-invalid="null" />
<argument type="service" id="twig.form.renderer" />
</service>
<service id="twig.form.engine" class="%twig.form.engine.class%" public="false">
<argument>%twig.form.resources%</argument>
</service>
<service id="twig.form.renderer" class="%twig.form.renderer.class%" public="false">
<argument type="service" id="twig.form.engine" />
<argument type="service" id="form.csrf_provider" on-invalid="null" />
</service>
<service id="twig.translation.extractor" class="%twig.translation.extractor.class%">
<argument type="service" id="twig" />
<tag name="translation.extractor" alias="twig" />

View File

@ -86,7 +86,7 @@
margin-left: -1px;
bottom: 38px;
border-bottom: 1px solid #fff;
border-bottom-width: 0;
}
.sf-toolbar-block .sf-toolbar-info:empty {
@ -216,9 +216,9 @@
}
.sf-toolbar-block .sf-toolbar-info {
top: 38px;
top: 39px;
bottom: auto;
border-top: 1px solid #fff;
border-top-width: 0;
border-bottom: 1px solid #bbb;
}
{% endif %}

View File

@ -13,4 +13,4 @@ CHANGELOG
* added GenericEvent event class
* added the possibility for subscribers to subscribe several times for the
same event
* added UnmodifiableEventDispatcher
* added ImmutableEventDispatcher

View File

@ -16,7 +16,7 @@ namespace Symfony\Component\EventDispatcher;
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class UnmodifiableEventDispatcher implements EventDispatcherInterface
class ImmutableEventDispatcher implements EventDispatcherInterface
{
/**
* The proxied dispatcher.

View File

@ -12,13 +12,13 @@
namespace Symfony\Component\EventDispatcher\Tests;
use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\EventDispatcher\UnmodifiableEventDispatcher;
use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class UnmodifiableEventDispatcherTest extends \PHPUnit_Framework_TestCase
class ImmutableEventDispatcherTest extends \PHPUnit_Framework_TestCase
{
/**
* @var \PHPUnit_Framework_MockObject_MockObject
@ -26,14 +26,14 @@ class UnmodifiableEventDispatcherTest extends \PHPUnit_Framework_TestCase
private $innerDispatcher;
/**
* @var UnmodifiableEventDispatcher
* @var ImmutableEventDispatcher
*/
private $dispatcher;
protected function setUp()
{
$this->innerDispatcher = $this->getMock('Symfony\Component\EventDispatcher\EventDispatcherInterface');
$this->dispatcher = new UnmodifiableEventDispatcher($this->innerDispatcher);
$this->dispatcher = new ImmutableEventDispatcher($this->innerDispatcher);
}
public function testDispatchDelegates()

View File

@ -66,7 +66,7 @@ class Finder implements \IteratorAggregate, \Countable
*/
public static function create()
{
return new self();
return new static();
}
/**

View File

@ -0,0 +1,206 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
/**
* Default implementation of {@link FormRendererEngineInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
abstract class AbstractRendererEngine implements FormRendererEngineInterface
{
/**
* The variable in {@link FormViewInterface} used as cache key.
*/
const CACHE_KEY_VAR = 'full_block_name';
/**
* @var array
*/
protected $defaultThemes;
/**
* @var array
*/
protected $themes = array();
/**
* @var array
*/
protected $resources = array();
/**
* @var array
*/
private $resourceHierarchyLevels = array();
/**
* Creates a new renderer engine.
*
* @param array $defaultThemes The default themes. The type of these
* themes is open to the implementation.
*/
public function __construct(array $defaultThemes = array())
{
$this->defaultThemes = $defaultThemes;
}
/**
* {@inheritdoc}
*/
public function setTheme(FormViewInterface $view, $themes)
{
$cacheKey = $view->getVar(self::CACHE_KEY_VAR);
// Do not cast, as casting turns objects into arrays of properties
$this->themes[$cacheKey] = is_array($themes) ? $themes : array($themes);
// Unset instead of resetting to an empty array, in order to allow
// implementations (like TwigRendererEngine) to check whether $cacheKey
// is set at all.
unset($this->resources[$cacheKey]);
unset($this->resourceHierarchyLevels[$cacheKey]);
}
/**
* {@inheritdoc}
*/
public function getResourceForBlock(FormViewInterface $view, $block)
{
$cacheKey = $view->getVar(self::CACHE_KEY_VAR);
if (!isset($this->resources[$cacheKey][$block])) {
$this->loadResourceForBlock($cacheKey, $view, $block);
}
return $this->resources[$cacheKey][$block];
}
/**
* {@inheritdoc}
*/
public function getResourceForBlockHierarchy(FormViewInterface $view, array $blockHierarchy, $hierarchyLevel)
{
$cacheKey = $view->getVar(self::CACHE_KEY_VAR);
$block = $blockHierarchy[$hierarchyLevel];
if (!isset($this->resources[$cacheKey][$block])) {
$this->loadResourceForBlockHierarchy($cacheKey, $view, $blockHierarchy, $hierarchyLevel);
}
return $this->resources[$cacheKey][$block];
}
/**
* {@inheritdoc}
*/
public function getResourceHierarchyLevel(FormViewInterface $view, array $blockHierarchy, $hierarchyLevel)
{
$cacheKey = $view->getVar(self::CACHE_KEY_VAR);
$block = $blockHierarchy[$hierarchyLevel];
if (!isset($this->resources[$cacheKey][$block])) {
$this->loadResourceForBlockHierarchy($cacheKey, $view, $blockHierarchy, $hierarchyLevel);
}
// If $block was previously rendered loaded with loadTemplateForBlock(), the template
// is cached but the hierarchy level is not. In this case, we know that the block
// exists at this very hierarchy level, so we can just set it.
if (!isset($this->resourceHierarchyLevels[$cacheKey][$block])) {
$this->resourceHierarchyLevels[$cacheKey][$block] = $hierarchyLevel;
}
return $this->resourceHierarchyLevels[$cacheKey][$block];
}
/**
* Loads the cache with the resource for a given block name.
*
* @see getResourceForBlock()
*
* @param string $cacheKey The cache key of the form view.
* @param FormViewInterface $view The form view for finding the applying themes.
* @param string $block The name of the block to load.
*
* @return Boolean True if the resource could be loaded, false otherwise.
*/
abstract protected function loadResourceForBlock($cacheKey, FormViewInterface $view, $block);
/**
* Loads the cache with the resource for a specific level of a block hierarchy.
*
* @see getResourceForBlockHierarchy()
*
* @param string $cacheKey The cache key used for storing the
* resource.
* @param FormViewInterface $view The form view for finding the applying
* themes.
* @param array $blockHierarchy The block hierarchy, with the most
* specific block name at the end.
* @param integer $hierarchyLevel The level in the block hierarchy that
* should be loaded.
*
* @return Boolean True if the resource could be loaded, false otherwise.
*/
private function loadResourceForBlockHierarchy($cacheKey, FormViewInterface $view, array $blockHierarchy, $hierarchyLevel)
{
$block = $blockHierarchy[$hierarchyLevel];
// Try to find a template for that block
if ($this->loadResourceForBlock($cacheKey, $view, $block)) {
// If loadTemplateForBlock() returns true, it was able to populate the
// cache. The only missing thing is to set the hierarchy level at which
// the template was found.
$this->resourceHierarchyLevels[$cacheKey][$block] = $hierarchyLevel;
return true;
}
if ($hierarchyLevel > 0) {
$parentLevel = $hierarchyLevel - 1;
$parentBlock = $blockHierarchy[$parentLevel];
// The next two if statements contain slightly duplicated code. This is by intention
// and tries to avoid execution of unnecessary checks in order to increase performance.
if (isset($this->resources[$cacheKey][$parentBlock])) {
// It may happen that the parent block is already loaded, but its level is not.
// In this case, the parent block must have been loaded by loadResourceForBlock(),
// which does not check the hierarchy of the block. Subsequently the block must have
// been found directly on the parent level.
if (!isset($this->resourceHierarchyLevels[$cacheKey][$parentBlock])) {
$this->resourceHierarchyLevels[$cacheKey][$parentBlock] = $parentLevel;
}
// Cache the shortcuts for further accesses
$this->resources[$cacheKey][$block] = $this->resources[$cacheKey][$parentBlock];
$this->resourceHierarchyLevels[$cacheKey][$block] = $this->resourceHierarchyLevels[$cacheKey][$parentBlock];
return true;
}
if ($this->loadResourceForBlockHierarchy($cacheKey, $view, $blockHierarchy, $parentLevel)) {
// Cache the shortcuts for further accesses
$this->resources[$cacheKey][$block] = $this->resources[$cacheKey][$parentBlock];
$this->resourceHierarchyLevels[$cacheKey][$block] = $this->resourceHierarchyLevels[$cacheKey][$parentBlock];
return true;
}
}
// Cache the result for further accesses
$this->resources[$cacheKey][$block] = false;
$this->resourceHierarchyLevels[$cacheKey][$block] = false;
return false;
}
}

View File

@ -31,7 +31,6 @@ CHANGELOG
* ArrayToChoicesTransformer to ChoicesToValuesTransformer
* ScalarToChoiceTransformer to ChoiceToValueTransformer
to be consistent with the naming in ChoiceListInterface.
* [BC BREAK] removed FormUtil::toArrayKey() and FormUtil::toArrayKeys().
They were merged into ChoiceList and have no public equivalent anymore.
* choice fields now throw a FormException if neither the "choices" nor the
"choice_list" option is set
@ -141,7 +140,7 @@ CHANGELOG
* deprecated `hasChildren` in Form and FormBuilder in favor of `count`
* FormBuilder now implements \IteratorAggregate
* [BC BREAK] compound forms now always need a data mapper
* FormBuilder now maintains the order when explicitely adding form builders as children
* FormBuilder now maintains the order when explicitly adding form builders as children
* ChoiceType now doesn't add the empty value anymore if the choices already contain an empty element
* DateType, TimeType and DateTimeType now show empty values again if not required
* [BC BREAK] fixed rendering of errors for DateType, BirthdayType and similar ones
@ -173,3 +172,10 @@ CHANGELOG
* [BC BREAK] Rows of a collection field cannot be themed individually anymore. All rows in the collection
field now have the same block names, which contains "entry" where it previously contained the row index.
* [BC BREAK] When registering a type through the DI extension, the tag alias has to match the actual type name.
* added FormRendererInterface, FormRendererEngineInterface and implementations of these interfaces
* [BC BREAK] removed the following methods from FormUtil:
* `toArrayKey`
* `toArrayKeys`
* `isChoiceGroup`
* `isChoiceSelected`
* added method `block` to FormHelper and deprecated `renderBlock` instead

View File

@ -157,7 +157,7 @@ class ChoiceType extends AbstractType
return $options['required'] ? null : '';
};
$emptyValueFilter = function (Options $options, $emptyValue) {
$emptyValueNormalizer = function (Options $options, $emptyValue) {
if ($options['multiple'] || $options['expanded']) {
// never use an empty value for these cases
return null;
@ -186,8 +186,8 @@ class ChoiceType extends AbstractType
'compound' => $compound,
));
$resolver->setFilters(array(
'empty_value' => $emptyValueFilter,
$resolver->setNormalizers(array(
'empty_value' => $emptyValueNormalizer,
));
$resolver->setAllowedTypes(array(

View File

@ -74,7 +74,7 @@ class CollectionType extends AbstractType
*/
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$optionsFilter = function (Options $options, $value) {
$optionsNormalizer = function (Options $options, $value) {
$value['block_name'] = 'entry';
return $value;
@ -89,8 +89,8 @@ class CollectionType extends AbstractType
'options' => array(),
));
$resolver->setFilters(array(
'options' => $optionsFilter,
$resolver->setNormalizers(array(
'options' => $optionsNormalizer,
));
}

View File

@ -164,7 +164,7 @@ class DateType extends AbstractType
return $options['required'] ? null : '';
};
$emptyValueFilter = function (Options $options, $emptyValue) use ($emptyValueDefault) {
$emptyValueNormalizer = function (Options $options, $emptyValue) use ($emptyValueDefault) {
if (is_array($emptyValue)) {
$default = $emptyValueDefault($options);
@ -216,8 +216,8 @@ class DateType extends AbstractType
'compound' => $compound,
));
$resolver->setFilters(array(
'empty_value' => $emptyValueFilter,
$resolver->setNormalizers(array(
'empty_value' => $emptyValueNormalizer,
));
$resolver->setAllowedValues(array(

View File

@ -134,7 +134,7 @@ class TimeType extends AbstractType
return $options['required'] ? null : '';
};
$emptyValueFilter = function (Options $options, $emptyValue) use ($emptyValueDefault) {
$emptyValueNormalizer = function (Options $options, $emptyValue) use ($emptyValueDefault) {
if (is_array($emptyValue)) {
$default = $emptyValueDefault($options);
@ -186,8 +186,8 @@ class TimeType extends AbstractType
'compound' => $compound,
));
$resolver->setFilters(array(
'empty_value' => $emptyValueFilter,
$resolver->setNormalizers(array(
'empty_value' => $emptyValueNormalizer,
));
$resolver->setAllowedValues(array(

View File

@ -46,7 +46,12 @@ class DependencyInjectionExtension implements FormExtensionInterface
$type = $this->container->get($this->typeServiceIds[$name]);
if ($type->getName() !== $name) {
throw new \InvalidArgumentException(sprintf('The type name specified for the service %s does not match the actual name', $this->typeServiceIds[$name]));
throw new \InvalidArgumentException(
sprintf('The type name specified for the service "%s" does not match the actual name. Expected "%s", given "%s"',
$this->typeServiceIds[$name],
$name,
$type->getName()
));
}
return $type;

View File

@ -0,0 +1,125 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form\Extension\Templating;
use Symfony\Component\Form\AbstractRendererEngine;
use Symfony\Component\Form\FormViewInterface;
use Symfony\Component\Templating\EngineInterface;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class TemplatingRendererEngine extends AbstractRendererEngine
{
/**
* @var EngineInterface
*/
private $engine;
public function __construct(EngineInterface $engine, array $defaultThemes = array())
{
parent::__construct($defaultThemes);
$this->engine = $engine;
}
/**
* {@inheritdoc}
*/
public function renderBlock(FormViewInterface $view, $resource, $block, array $variables = array())
{
return trim($this->engine->render($resource, $variables));
}
/**
* Loads the cache with the resource for a given block name.
*
* This implementation tries to load as few blocks as possible, since each block
* is represented by a template on the file system.
*
* @see getResourceForBlock()
*
* @param string $cacheKey The cache key of the form view.
* @param FormViewInterface $view The form view for finding the applying themes.
* @param string $block The name of the block to load.
*
* @return Boolean True if the resource could be loaded, false otherwise.
*/
protected function loadResourceForBlock($cacheKey, FormViewInterface $view, $block)
{
// Recursively try to find the block in the themes assigned to $view,
// then of its parent form, then of the parent form of the parent and so on.
// When the root form is reached in this recursion, also the default
// themes are taken into account.
// Check each theme whether it contains the searched block
if (isset($this->themes[$cacheKey])) {
for ($i = count($this->themes[$cacheKey]) - 1; $i >= 0; --$i) {
if ($this->loadResourceFromTheme($cacheKey, $block, $this->themes[$cacheKey][$i])) {
return true;
}
}
}
// Check the default themes once we reach the root form without success
if (!$view->hasParent()) {
for ($i = count($this->defaultThemes) - 1; $i >= 0; --$i) {
if ($this->loadResourceFromTheme($cacheKey, $block, $this->defaultThemes[$i])) {
return true;
}
}
}
// If we did not find anything in the themes of the current view, proceed
// with the themes of the parent view
if ($view->hasParent()) {
$parentCacheKey = $view->getParent()->getVar(self::CACHE_KEY_VAR);
if (!isset($this->resources[$parentCacheKey][$block])) {
$this->loadResourceForBlock($parentCacheKey, $view->getParent(), $block);
}
// If a template exists in the parent themes, cache that template
// for the current theme as well to speed up further accesses
if ($this->resources[$parentCacheKey][$block]) {
$this->resources[$cacheKey][$block] = $this->resources[$parentCacheKey][$block];
return true;
}
}
// Cache that we didn't find anything to speed up further accesses
$this->resources[$cacheKey][$block] = false;
return false;
}
/**
* Tries to load the resource for a block from a theme.
*
* @param string $cacheKey The cache key for storing the resource.
* @param string $block The name of the block to load a resource for.
* @param mixed $theme The theme to load the block from.
*
* @return Boolean True if the resource could be loaded, false otherwise.
*/
protected function loadResourceFromTheme($cacheKey, $block, $theme)
{
if ($this->engine->exists($templateName = $theme . ':' . $block . '.html.php')) {
$this->resources[$cacheKey][$block] = $templateName;
return true;
}
return false;
}
}

View File

@ -59,7 +59,7 @@ class FormTypeValidatorExtension extends AbstractTypeExtension
};
// Make sure that validation groups end up as null, closure or array
$validationGroupsFilter = function (Options $options, $groups) {
$validationGroupsNormalizer = function (Options $options, $groups) {
if (empty($groups)) {
return null;
}
@ -72,7 +72,7 @@ class FormTypeValidatorExtension extends AbstractTypeExtension
};
// Constraint should always be converted to an array
$constraintsFilter = function (Options $options, $constraints) {
$constraintsNormalizer = function (Options $options, $constraints) {
return is_object($constraints) ? array($constraints) : (array) $constraints;
};
@ -88,9 +88,9 @@ class FormTypeValidatorExtension extends AbstractTypeExtension
'post_max_size_message' => 'The uploaded file was too large. Please try to upload a smaller file.',
));
$resolver->setFilters(array(
'validation_groups' => $validationGroupsFilter,
'constraints' => $constraintsFilter,
$resolver->setNormalizers(array(
'validation_groups' => $validationGroupsNormalizer,
'constraints' => $constraintsNormalizer,
));
}

View File

@ -119,6 +119,26 @@ class Form implements \IteratorAggregate, FormInterface
*/
private $synchronized = true;
/**
* Whether the form's data has been initialized.
*
* When the data is initialized with its default value, that default value
* is passed through the transformer chain in order to synchronize the
* model, normalized and view format for the first time. This is done
* lazily in order to save performance when {@link setData()} is called
* manually, making the initialization with the configured default value
* superfluous.
*
* @var Boolean
*/
private $initialized = false;
/**
* Whether setData() is currently being called.
* @var Boolean
*/
private $lockSetData = false;
/**
* Creates a new form based on the given configuration.
*
@ -126,8 +146,8 @@ class Form implements \IteratorAggregate, FormInterface
*/
public function __construct(FormConfigInterface $config)
{
if (!$config instanceof UnmodifiableFormConfig) {
$config = new UnmodifiableFormConfig($config);
if (!$config instanceof ImmutableFormConfig) {
$config = new ImmutableFormConfig($config);
}
// Compound forms always need a data mapper, otherwise calls to
@ -138,8 +158,6 @@ class Form implements \IteratorAggregate, FormInterface
}
$this->config = $config;
$this->setData($config->getData());
}
public function __clone()
@ -152,7 +170,7 @@ class Form implements \IteratorAggregate, FormInterface
/**
* Returns the configuration of the form.
*
* @return UnmodifiableFormConfig The form's immutable configuration.
* @return ImmutableFormConfig The form's immutable configuration.
*/
public function getConfig()
{
@ -327,13 +345,16 @@ class Form implements \IteratorAggregate, FormInterface
/**
* Updates the form with default data.
*
* @param array $modelData The data formatted as expected for the underlying object
* @param mixed $modelData The data formatted as expected for the underlying object
*
* @return Form The current form
*/
public function setData($modelData)
{
if ($this->bound) {
// If the form is bound while disabled, it is set to bound, but the data is not
// changed. In such cases (i.e. when the form is not initialized yet) don't
// abort this method.
if ($this->bound && $this->initialized) {
throw new AlreadyBoundException('You cannot change the data of a bound form');
}
@ -346,6 +367,12 @@ class Form implements \IteratorAggregate, FormInterface
$modelData = clone $modelData;
}
if ($this->lockSetData) {
throw new FormException('A cycle was detected. Listeners to the PRE_SET_DATA event must not call setData(). You should call setData() on the FormEvent object instead.');
}
$this->lockSetData = true;
// Hook to change content of the data
$event = new FormEvent($this, $modelData);
$this->config->getEventDispatcher()->dispatch(FormEvents::PRE_SET_DATA, $event);
@ -395,8 +422,12 @@ class Form implements \IteratorAggregate, FormInterface
$this->normData = $normData;
$this->viewData = $viewData;
$this->synchronized = true;
$this->initialized = true;
$this->lockSetData = false;
if ($this->config->getCompound()) {
// It is not necessary to invoke this method if the form doesn't have children,
// even if the form is compound.
if (count($this->children) > 0) {
// Update child forms from the data
$this->config->getDataMapper()->mapDataToForms($viewData, $this->children);
}
@ -414,9 +445,29 @@ class Form implements \IteratorAggregate, FormInterface
*/
public function getData()
{
if (!$this->initialized) {
$this->setData($this->config->getData());
}
return $this->modelData;
}
/**
* Returns the normalized data of the form.
*
* @return mixed When the form is not bound, the default data is returned.
* When the form is bound, the normalized bound data is
* returned if the form is valid, null otherwise.
*/
public function getNormData()
{
if (!$this->initialized) {
$this->setData($this->config->getData());
}
return $this->normData;
}
/**
* Returns the data transformed by the value transformer.
*
@ -424,6 +475,10 @@ class Form implements \IteratorAggregate, FormInterface
*/
public function getViewData()
{
if (!$this->initialized) {
$this->setData($this->config->getData());
}
return $this->viewData;
}
@ -543,7 +598,9 @@ class Form implements \IteratorAggregate, FormInterface
}
// Merge form data from children into existing view data
if ($this->config->getCompound()) {
// It is not necessary to invoke this method if the form has no children,
// even if it is compound.
if (count($this->children) > 0) {
$this->config->getDataMapper()->mapFormsToData($this->children, $viewData);
}
@ -573,6 +630,7 @@ class Form implements \IteratorAggregate, FormInterface
$this->viewData = $viewData;
$this->extraData = $extraData;
$this->synchronized = $synchronized;
$this->initialized = true;
$event = new FormEvent($this, $viewData);
$this->config->getEventDispatcher()->dispatch(FormEvents::POST_BIND, $event);
@ -604,18 +662,6 @@ class Form implements \IteratorAggregate, FormInterface
return $this->bind($request);
}
/**
* Returns the normalized data of the form.
*
* @return mixed When the form is not bound, the default data is returned.
* When the form is bound, the normalized bound data is
* returned if the form is valid, null otherwise.
*/
public function getNormData()
{
return $this->normData;
}
/**
* Adds an error to this form.
*
@ -833,11 +879,32 @@ class Form implements \IteratorAggregate, FormInterface
throw new FormException('You cannot add children to a simple form. Maybe you should set the option "compound" to true?');
}
// Obtain the view data
$viewData = null;
// If setData() is currently being called, there is no need to call
// mapDataToForms() here, as mapDataToForms() is called at the end
// of setData() anyway. Not doing this check leads to an endless
// recursion when initializing the form lazily and an event listener
// (such as ResizeFormListener) adds fields depending on the data:
//
// * setData() is called, the form is not initialized yet
// * add() is called by the listener (setData() is not complete, so
// the form is still not initialized)
// * getViewData() is called
// * setData() is called since the form is not initialized yet
// * ... endless recursion ...
if (!$this->lockSetData) {
$viewData = $this->getViewData();
}
$this->children[$child->getName()] = $child;
$child->setParent($this);
$this->config->getDataMapper()->mapDataToForms($this->getViewData(), array($child));
if (!$this->lockSetData) {
$this->config->getDataMapper()->mapDataToForms($viewData, array($child));
}
return $this;
}

View File

@ -0,0 +1,350 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Exception\FormException;
use Symfony\Component\Form\Extension\Csrf\CsrfProvider\CsrfProviderInterface;
/**
* Renders a form into HTML using a rendering engine.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class FormRenderer implements FormRendererInterface
{
/**
* @var FormRendererEngineInterface
*/
private $engine;
/**
* @var CsrfProviderInterface
*/
private $csrfProvider;
/**
* @var array
*/
private $blockHierarchyMap = array();
/**
* @var array
*/
private $hierarchyLevelMap = array();
/**
* @var array
*/
private $variableMap = array();
/**
* @var array
*/
private $stack = array();
public function __construct(FormRendererEngineInterface $engine, CsrfProviderInterface $csrfProvider = null)
{
$this->engine = $engine;
$this->csrfProvider = $csrfProvider;
}
/**
* {@inheritdoc}
*/
public function getEngine()
{
return $this->engine;
}
/**
* {@inheritdoc}
*/
public function setTheme(FormViewInterface $view, $themes)
{
$this->engine->setTheme($view, $themes);
}
/**
* {@inheritdoc}
*/
public function renderEnctype(FormViewInterface $view)
{
return $this->renderSection($view, 'enctype');
}
/**
* {@inheritdoc}
*/
public function renderRow(FormViewInterface $view, array $variables = array())
{
return $this->renderSection($view, 'row', $variables);
}
/**
* {@inheritdoc}
*/
public function renderRest(FormViewInterface $view, array $variables = array())
{
return $this->renderSection($view, 'rest', $variables);
}
/**
* {@inheritdoc}
*/
public function renderWidget(FormViewInterface $view, array $variables = array())
{
return $this->renderSection($view, 'widget', $variables);
}
/**
* {@inheritdoc}
*/
public function renderErrors(FormViewInterface $view)
{
return $this->renderSection($view, 'errors');
}
/**
* {@inheritdoc}
*/
public function renderLabel(FormViewInterface $view, $label = null, array $variables = array())
{
if ($label !== null) {
$variables += array('label' => $label);
}
return $this->renderSection($view, 'label', $variables);
}
/**
* {@inheritdoc}
*/
public function renderCsrfToken($intention)
{
if (null === $this->csrfProvider) {
throw new \BadMethodCallException('CSRF token can only be generated if a CsrfProviderInterface is injected in the constructor.');
}
return $this->csrfProvider->generateCsrfToken($intention);
}
/**
* {@inheritdoc}
*/
public function renderBlock($block, array $variables = array())
{
if (0 == count($this->stack)) {
throw new FormException('This method should only be called while rendering a form element.');
}
list($view, $scopeVariables) = end($this->stack);
$resource = $this->engine->getResourceForBlock($view, $block);
if (!$resource) {
throw new FormException(sprintf('No block "%s" found while rendering the form.', $block));
}
// Merge the passed with the existing attributes
if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
$variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
}
// Merge the passed with the exist *label* attributes
if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
$variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
}
// Do not use array_replace_recursive(), otherwise array variables
// cannot be overwritten
$variables = array_replace($scopeVariables, $variables);
return $this->engine->renderBlock($view, $resource, $block, $variables);
}
/**
* {@inheritdoc}
*/
public function isChoiceGroup($choice)
{
return is_array($choice) || $choice instanceof \Traversable;
}
/**
* {@inheritdoc}
*/
public function isChoiceSelected(FormViewInterface $view, ChoiceView $choice)
{
$value = $view->getVar('value');
$choiceValue = $choice->getValue();
if (is_array($value)) {
return false !== array_search($choiceValue, $value, true);
}
return $choiceValue === $value;
}
/**
* {@inheritdoc}
*/
public function humanize($text)
{
return ucfirst(trim(strtolower(preg_replace('/[_\s]+/', ' ', $text))));
}
/**
* Renders the given section of a form view.
*
* @param FormViewInterface $view The form view.
* @param string $section The name of the section to render.
* @param array $variables The variables to pass to the template.
*
* @return string The HTML markup.
*
* @throws Exception\FormException If no fitting template was found.
*/
protected function renderSection(FormViewInterface $view, $section, array $variables = array())
{
$renderOnlyOnce = in_array($section, array('row', 'widget'));
if ($renderOnlyOnce && $view->isRendered()) {
return '';
}
// The cache key for storing the variables and types
$mapKey = $uniqueBlockName = $view->getVar('full_block_name') . '_' . $section;
// In templates, we have to deal with two kinds of block hierarchies:
//
// +---------+ +---------+
// | Theme B | -------> | Theme A |
// +---------+ +---------+
//
// form_widget -------> form_widget
// ^
// |
// choice_widget -----> choice_widget
//
// The first kind of hierarchy is the theme hierarchy. This allows to
// override the block "choice_widget" from Theme A in the extending
// Theme B. This kind of inheritance needs to be supported by the
// template engine and, for example, offers "parent()" or similar
// functions to fall back from the custom to the parent implementation.
//
// The second kind of hierarchy is the form type hierarchy. This allows
// to implement a custom "choice_widget" block (no matter in which theme),
// or to fallback to the block of the parent type, which would be
// "form_widget" in this example (again, no matter in which theme).
// If the designer wants to explicitly fallback to "form_widget" in his
// custom "choice_widget", for example because he only wants to wrap
// a <div> around the original implementation, he can simply call the
// widget() function again to render the block for the parent type.
//
// The second kind is implemented in the following blocks.
if (!isset($this->blockHierarchyMap[$mapKey])) {
// INITIAL CALL
// Calculate the hierarchy of template blocks and start on
// the bottom level of the hierarchy (= "_<id>_<section>" block)
$blockHierarchy = array();
foreach ($view->getVar('types') as $type) {
$blockHierarchy[] = $type . '_' . $section;
}
$blockHierarchy[] = $uniqueBlockName;
$hierarchyLevel = count($blockHierarchy) - 1;
// The default variable scope contains all view variables, merged with
// the variables passed explicitly to the helper
$scopeVariables = $view->getVars();
} else {
// RECURSIVE CALL
// If a block recursively calls renderSection() again, resume rendering
// using the parent type in the hierarchy.
$blockHierarchy = $this->blockHierarchyMap[$mapKey];
$hierarchyLevel = $this->hierarchyLevelMap[$mapKey] - 1;
// Reuse the current scope and merge it with the explicitly passed variables
$scopeVariables = $this->variableMap[$mapKey];
}
// Load the resource where this block can be found
$resource = $this->engine->getResourceForBlockHierarchy($view, $blockHierarchy, $hierarchyLevel);
// Update the current hierarchy level to the one at which the resource was
// found. For example, if looking for "choice_widget", but only a resource
// is found for its parent "form_widget", then the level is updated here
// to the parent level.
$hierarchyLevel = $this->engine->getResourceHierarchyLevel($view, $blockHierarchy, $hierarchyLevel);
// The actually existing block name in $resource
$block = $blockHierarchy[$hierarchyLevel];
// Escape if no resource exists for this block
if (!$resource) {
throw new FormException(sprintf(
'Unable to render the form as none of the following blocks exist: "%s".',
implode('", "', array_reverse($blockHierarchy))
));
}
// Merge the passed with the existing attributes
if (isset($variables['attr']) && isset($scopeVariables['attr'])) {
$variables['attr'] = array_replace($scopeVariables['attr'], $variables['attr']);
}
// Merge the passed with the exist *label* attributes
if (isset($variables['label_attr']) && isset($scopeVariables['label_attr'])) {
$variables['label_attr'] = array_replace($scopeVariables['label_attr'], $variables['label_attr']);
}
// Do not use array_replace_recursive(), otherwise array variables
// cannot be overwritten
$variables = array_replace($scopeVariables, $variables);
// In order to make recursive calls possible, we need to store the block hierarchy,
// the current level of the hierarchy and the variables so that this method can
// resume rendering one level higher of the hierarchy when it is called recursively.
//
// We need to store these values in maps (associative arrays) because within a
// call to widget() another call to widget() can be made, but for a different view
// object. These nested calls should not override each other.
$this->blockHierarchyMap[$mapKey] = $blockHierarchy;
$this->hierarchyLevelMap[$mapKey] = $hierarchyLevel;
$this->variableMap[$mapKey] = $variables;
// We also need to store the view and the variables so that we can render custom
// blocks with renderBlock() using the same themes and variables as in the outer
// block.
//
// A stack is sufficient for this purpose, because renderBlock() always accesses
// the immediate next outer scope, which is always stored at the end of the stack.
$this->stack[] = array($view, $variables);
// Do the rendering
$html = $this->engine->renderBlock($view, $resource, $block, $variables);
// Clear the stack
array_pop($this->stack);
// Clear the maps
unset($this->blockHierarchyMap[$mapKey]);
unset($this->hierarchyLevelMap[$mapKey]);
unset($this->variableMap[$mapKey]);
if ($renderOnlyOnce) {
$view->setRendered();
}
return $html;
}
}

View File

@ -0,0 +1,146 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
/**
* Adapter for rendering form templates with a specific templating engine.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface FormRendererEngineInterface
{
/**
* Sets the theme(s) to be used for rendering a view and its children.
*
* @param FormViewInterface $view The view to assign the theme(s) to.
* @param mixed $themes The theme(s). The type of these themes
* is open to the implementation.
*/
public function setTheme(FormViewInterface $view, $themes);
/**
* Returns the resource for a block name.
*
* The resource is first searched in the themes attached to $view, then
* in the themes of its parent view and so on, until a resource was found.
*
* The type of the resource is decided by the implementation. The resource
* is later passed to {@link renderBlock()} by the rendering algorithm.
*
* @param FormViewInterface $view The view for determining the used themes.
* First the themes attached directly to the
* view with {@link setTheme()} are considered,
* then the ones of its parent etc.
* @param string $block The name of the block to render.
*
* @return mixed The renderer resource or false, if none was found.
*/
public function getResourceForBlock(FormViewInterface $view, $block);
/**
* Returns the resource for a block hierarchy.
*
* A block hierarchy is an array which starts with the root of the hierarchy
* and continues with the child of that root, the child of that child etc.
* The following is an example for a block hierarchy:
*
* <code>
* form_widget
* text_widget
* url_widget
* </code>
*
* In this example, "url_widget" is the most specific block, while the other
* blocks are its ancestors in the hierarchy.
*
* The second parameter $hierarchyLevel determines the level of the hierarchy
* that should be rendered. For example, if $hierarchyLevel is 2 for the
* above hierarchy, the engine will first look for the block "url_widget",
* then, if that does not exist, for the block "text_widget" etc.
*
* The type of the resource is decided by the implementation. The resource
* is later passed to {@link renderBlock()} by the rendering algorithm.
*
* @param FormViewInterface $view The view for determining the used
* themes. First the themes attached
* directly to the view with
* {@link setTheme()} are considered,
* then the ones of its parent etc.
* @param array $blockHierarchy The block name hierarchy, with
* the root block at the beginning.
* @param integer $hierarchyLevel The level in the hierarchy at
* which to start looking. Level 0
* indicates the root block, i.e.
* the first element of $blockHierarchy.
*
* @return mixed The renderer resource or false, if none was found.
*/
public function getResourceForBlockHierarchy(FormViewInterface $view, array $blockHierarchy, $hierarchyLevel);
/**
* Returns the hierarchy level at which a resource can be found.
*
* A block hierarchy is an array which starts with the root of the hierarchy
* and continues with the child of that root, the child of that child etc.
* The following is an example for a block hierarchy:
*
* <code>
* form_widget
* text_widget
* url_widget
* </code>
*
* The second parameter $hierarchyLevel determines the level of the hierarchy
* that should be rendered.
*
* If we call this method with the hierarchy level 2, the engine will first
* look for a resource for block "url_widget". If such a resource exists,
* the method returns 2. Otherwise it tries to find a resource for block
* "text_widget" (at level 1) and, again, returns 1 if a resource was found.
* The method continues to look for resources until the root level was
* reached and nothing was found. In this case false is returned.
*
* The type of the resource is decided by the implementation. The resource
* is later passed to {@link renderBlock()} by the rendering algorithm.
*
* @param FormViewInterface $view The view for determining the used
* themes. First the themes attached
* directly to the view with
* {@link setTheme()} are considered,
* then the ones of its parent etc.
* @param array $blockHierarchy The block name hierarchy, with
* the root block at the beginning.
* @param integer $hierarchyLevel The level in the hierarchy at
* which to start looking. Level 0
* indicates the root block, i.e.
* the first element of $blockHierarchy.
*
* @return integer|Boolean The hierarchy level or false, if no resource was found.
*/
public function getResourceHierarchyLevel(FormViewInterface $view, array $blockHierarchy, $hierarchyLevel);
/**
* Renders a block in the given renderer resource.
*
* The resource can be obtained by calling {@link getResourceForBlock()}
* or {@link getResourceForBlockHierarchy()}. The type of the resource is
* decided by the implementation.
*
* @param FormViewInterface $view The view to render.
* @param mixed $resource The renderer resource.
* @param string $block The name of the block to render.
* @param array $variables The variables to pass to the template.
*
* @return string The HTML markup.
*/
public function renderBlock(FormViewInterface $view, $resource, $block, array $variables = array());
}

View File

@ -0,0 +1,180 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Component\Form;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
/**
* Renders a form into HTML.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface FormRendererInterface
{
/**
* Returns the engine used by this renderer.
*
* @return FormRendererEngineInterface The renderer engine.
*/
public function getEngine();
/**
* Sets the theme(s) to be used for rendering a view and its children.
*
* @param FormViewInterface $view The view to assign the theme(s) to.
* @param mixed $themes The theme(s). The type of these themes
* is open to the implementation.
*/
public function setTheme(FormViewInterface $view, $themes);
/**
* Renders the HTML enctype in the form tag, if necessary.
*
* Example usage templates:
*
* <form action="..." method="post" <?php echo $renderer->renderEnctype($form) ?>>
*
* @param FormViewInterface $view The view for which to render the encoding type
*
* @return string The HTML markup
*/
public function renderEnctype(FormViewInterface $view);
/**
* Renders the entire row for a form field.
*
* A row typically contains the label, errors and widget of a field.
*
* @param FormViewInterface $view The view for which to render the row
* @param array $variables Additional variables passed to the template
*
* @return string The HTML markup
*/
public function renderRow(FormViewInterface $view, array $variables = array());
/**
* Renders views which have not already been rendered.
*
* @param FormViewInterface $view The parent view
* @param array $variables An array of variables
*
* @return string The HTML markup
*/
public function renderRest(FormViewInterface $view, array $variables = array());
/**
* Renders the HTML for a given view.
*
* Example usage:
*
* <?php echo $renderer->renderWidget($form) ?>
*
* You can pass options during the call:
*
* <?php echo $renderer->renderWidget($form, array('attr' => array('class' => 'foo'))) ?>
*
* <?php echo $renderer->renderWidget($form, array('separator' => '+++++)) ?>
*
* @param FormViewInterface $view The view for which to render the widget
* @param array $variables Additional variables passed to the template
*
* @return string The HTML markup
*/
public function renderWidget(FormViewInterface $view, array $variables = array());
/**
* Renders the errors of the given view.
*
* @param FormViewInterface $view The view to render the errors for
*
* @return string The HTML markup
*/
public function renderErrors(FormViewInterface $view);
/**
* Renders the label of the given view.
*
* @param FormViewInterface $view The view for which to render the label
* @param string $label The label
* @param array $variables Additional variables passed to the template
*
* @return string The HTML markup
*/
public function renderLabel(FormViewInterface $view, $label = null, array $variables = array());
/**
* Renders a named block of the form theme.
*
* @param string $block The name of the block.
* @param array $variables The variables to pass to the template.
*
* @return string The HTML markup
*/
public function renderBlock($block, array $variables = array());
/**
* Renders a CSRF token.
*
* Use this helper for CSRF protection without the overhead of creating a
* form.
*
* <code>
* <input type="hidden" name="token" value="<?php $renderer->renderCsrfToken('rm_user_'.$user->getId()) ?>">
* </code>
*
* Check the token in your action using the same intention.
*
* <code>
* $csrfProvider = $this->get('form.csrf_provider');
* if (!$csrfProvider->isCsrfTokenValid('rm_user_'.$user->getId(), $token)) {
* throw new \RuntimeException('CSRF attack detected.');
* }
* </code>
*
* @param string $intention The intention of the protected action
*
* @return string A CSRF token
*/
public function renderCsrfToken($intention);
/**
* Returns whether the given choice is a group.
*
* @param mixed $choice A choice
*
* @return Boolean Whether the choice is a group
*/
public function isChoiceGroup($choice);
/**
* Returns whether the given choice is selected.
*
* @param FormViewInterface $view The view of the choice field
* @param ChoiceView $choice The choice to check
*
* @return Boolean Whether the choice is selected
*/
public function isChoiceSelected(FormViewInterface $view, ChoiceView $choice);
/**
* Makes a technical name human readable.
*
* Sequences of underscores are replaced by single spaces. The first letter
* of the resulting string is capitalized, while all other letters are
* turned to lowercase.
*
* @param string $text The text to humanize.
*
* @return string The humanized text.
*/
public function humanize($text);
}

View File

@ -12,14 +12,14 @@
namespace Symfony\Component\Form;
use Symfony\Component\Form\Util\PropertyPath;
use Symfony\Component\EventDispatcher\UnmodifiableEventDispatcher;
use Symfony\Component\EventDispatcher\ImmutableEventDispatcher;
/**
* A read-only form configuration.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class UnmodifiableFormConfig implements FormConfigInterface
class ImmutableFormConfig implements FormConfigInterface
{
/**
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
@ -134,8 +134,8 @@ class UnmodifiableFormConfig implements FormConfigInterface
public function __construct(FormConfigInterface $config)
{
$dispatcher = $config->getEventDispatcher();
if (!$dispatcher instanceof UnmodifiableEventDispatcher) {
$dispatcher = new UnmodifiableEventDispatcher($dispatcher);
if (!$dispatcher instanceof ImmutableEventDispatcher) {
$dispatcher = new ImmutableEventDispatcher($dispatcher);
}
$this->dispatcher = $dispatcher;

View File

@ -545,7 +545,7 @@ abstract class AbstractDivLayoutTest extends AbstractLayoutTest
]
/following-sibling::div
[
./label
./label[.="child"]
/following-sibling::div
[
./div

View File

@ -20,7 +20,7 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\Form\Tests\Fixtures\FixedDataTransformer;
class FormTest extends AbstractFormTest
class CompoundFormTest extends AbstractFormTest
{
public function testValidIfAllChildrenAreValid()
{

Some files were not shown because too many files have changed in this diff Show More