Merge branch '2.7'

* 2.7: (60 commits)
  [Translation][Profiler] fixed Collect empty Messages.
  [VarDumper] Towards PHP7 support
  Fix currently broken tests
  [Form][choice] added choice_translation_domain to avoid trans options.
  [Translation][Profiler]  added the number of times a translation has been used.
  [DoctrineBridge] Removed useless code
  [Debug] Updated CHANGELOG
  [Debug] Use symfony_debug_backtrace() in FatalErrorException when available
  [Debug] Add debug extension to the test suite
  [Debug] Add symfony_debug_backtrace() that works with fatal errors
  [Form] Updated CHANGELOG and UPGRADE files
  [HttpKernel] Embed the original exception as previous to bounced exceptions
  Added feedback about the current symfony version
  Deprecated precision option in favor of scale
  [Enhancement] netbeans - force interactive shell when limited detection
  Automatically start server:run if server:start failed
  Tweaked some console command styles
  [FrameworkBundle] fixes displaying of deprecation notices.
  make date formats and number formats configurable
  Revert "Added missing changelog entry"
  ...

Conflicts:
	CHANGELOG-2.3.md
	CHANGELOG-2.6.md
	src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php
	src/Symfony/Bridge/Twig/composer.json
	src/Symfony/Bundle/FrameworkBundle/Resources/config/translation.xml
	src/Symfony/Bundle/FrameworkBundle/composer.json
	src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php
	src/Symfony/Bundle/TwigBundle/Extension/ActionsExtension.php
	src/Symfony/Bundle/TwigBundle/Extension/AssetsExtension.php
	src/Symfony/Component/Debug/CHANGELOG.md
	src/Symfony/Component/Debug/ErrorHandler.php
	src/Symfony/Component/Form/CHANGELOG.md
	src/Symfony/Component/VarDumper/Tests/CliDumperTest.php
	src/Symfony/Component/VarDumper/Tests/HtmlDumperTest.php
This commit is contained in:
Nicolas Grekas 2015-04-05 10:20:29 +02:00
commit 6a0b617c3d
182 changed files with 8320 additions and 1200 deletions

View File

@ -30,6 +30,7 @@ before_install:
- if [[ "$TRAVIS_PHP_VERSION" != *"nightly" ]]; then echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi;
- if [[ "$TRAVIS_PHP_VERSION" != *"nightly" ]] && [ $(php -r "echo PHP_MINOR_VERSION;") -le 4 ]; then echo "extension = apc.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi;
- if [[ "$TRAVIS_PHP_VERSION" != *"nightly" ]]; then (pecl install -f memcached-2.1.0 && echo "extension = memcache.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini) || echo "Let's continue without memcache extension"; fi;
- if [[ "$TRAVIS_PHP_VERSION" != *"nightly" ]]; then (cd src/Symfony/Component/Debug/Resources/ext && phpize && ./configure && make && echo "extension = $(pwd)/modules/symfony_debug.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini); fi;
- if [[ "$TRAVIS_PHP_VERSION" != *"nightly" ]]; then php -i; fi;
- sudo locale-gen fr_FR.UTF-8 && sudo update-locale
# Set the COMPOSER_ROOT_VERSION to the right version according to the branch being built

View File

@ -11,8 +11,8 @@ Symfony is the result of the work of many people who made the code better
- Jordi Boggiano (seldaek)
- Johannes S (johannes)
- Kris Wallsmith (kriswallsmith)
- Christophe Coevoet (stof)
- Nicolas Grekas (nicolas-grekas)
- Christophe Coevoet (stof)
- Jakub Zalas (jakubzalas)
- Pascal Borreli (pborreli)
- Hugo Hamon (hhamon)
@ -21,9 +21,9 @@ Symfony is the result of the work of many people who made the code better
- Ryan Weaver (weaverryan)
- Lukas Kahwe Smith (lsmith)
- Romain Neutron (romain)
- Christian Flothmann (xabbuh)
- Jeremy Mikola (jmikola)
- Jean-François Simon (jfsimon)
- Christian Flothmann (xabbuh)
- Benjamin Eberlei (beberlei)
- Igor Wiedler (igorw)
- Martin Hasoň (hason)
@ -37,16 +37,16 @@ Symfony is the result of the work of many people who made the code better
- stealth35 (stealth35)
- Alexander Mols (asm89)
- Bulat Shakirzyanov (avalanche123)
- Abdellatif Ait boudad (aitboudad)
- Francis Besset (francisbesset)
- Saša Stamenković (umpirsky)
- Henrik Bjørnskov (henrikbjorn)
- Miha Vrhovnik
- Kévin Dunglas (dunglas)
- Sarah Khalil (saro0h)
- Konstantin Kudryashov (everzet)
- Bilal Amarni (bamarni)
- Kévin Dunglas (dunglas)
- Florin Patan (florinpatan)
- Abdellatif Ait Boudad (aitboudad)
- Sarah Khalil (saro0h)
- Eric Clemmons (ericclemmons)
- Andrej Hudec (pulzarraider)
- Deni
@ -61,13 +61,14 @@ Symfony is the result of the work of many people who made the code better
- Daniel Holmes (dholmes)
- Bart van den Burg (burgov)
- Jordan Alliot (jalliot)
- Kevin Bond (kbond)
- John Wards (johnwards)
- Fran Moreno (franmomu)
- Antoine Hérault (herzult)
- Kevin Bond (kbond)
- Toni Uebernickel (havvg)
- Luis Cordova (cordoval)
- Antoine Hérault (herzult)
- Toni Uebernickel (havvg)
- Arnaud Le Blanc (arnaud-lb)
- Gábor Egyed (1ed)
- Tim Nagel (merk)
- Brice BERNARD (brikou)
- marc.weistroff
@ -78,7 +79,6 @@ Symfony is the result of the work of many people who made the code better
- Colin Frei
- Jérôme Tamarelle (gromnan)
- Adrien Brault (adrienbrault)
- Gábor Egyed (1ed)
- excelwebzone
- Jacob Dreesen (jdreesen)
- Fabien Pennequin (fabienpennequin)
@ -93,26 +93,29 @@ Symfony is the result of the work of many people who made the code better
- Daniel Gomes (danielcsgomes)
- Hidenori Goto (hidenorigoto)
- David Buchmann (dbu)
- Guilherme Blanco (guilhermeblanco)
- Jérémy DERUSSÉ (jderusse)
- Pablo Godel (pgodel)
- Eric GELOEN (gelo)
- Jérémie Augustin (jaugustin)
- Rafael Dohms (rdohms)
- Guilherme Blanco (guilhermeblanco)
- Tigran Azatyan (tigranazatyan)
- Javier Eguiluz (javier.eguiluz)
- Arnaud Kleinpeter (nanocom)
- Richard Shank (iampersistent)
- Dariusz Ruminski
- Clemens Tolboom
- Helmer Aaviksoo
- Sebastiaan Stok (sstok)
- Hiromi Hishida (77web)
- Matthieu Ouellette-Vachon (maoueh)
- Michał Pipa (michal.pipa)
- Issei Murasawa (issei_m)
- Amal Raghav (kertz)
- Jonathan Ingram (jonathaningram)
- Artur Kotyrba
- Rouven Weßling (realityking)
- Andréia Bohner (andreia)
- Dmitrii Chekaliuk (lazyhammer)
- Clément JOBEILI (dator)
- Dorian Villet (gnutix)
@ -123,14 +126,12 @@ Symfony is the result of the work of many people who made the code better
- Benjamin Dulau (dbenjamin)
- Matthias Pigulla (mpdude)
- Andreas Hucks (meandmymonkey)
- Andréia Bohner (andreia)
- Noel Guilbert (noel)
- Joel Wurtz (brouznouf)
- Charles Sarrazin (csarrazi)
- bronze1man
- sun (sun)
- Larry Garfield (crell)
- Issei Murasawa (issei_m)
- Martin Schuhfuß (usefulthink)
- Thomas Rabaix (rande)
- Matthieu Bontemps (mbontemps)
@ -163,6 +164,7 @@ Symfony is the result of the work of many people who made the code better
- Xavier Montaña Carreras (xmontana)
- Michele Orselli (orso)
- Chris Wilkinson (thewilkybarkid)
- Joshua Thijssen
- Xavier Perez
- Arjen Brouwer (arjenjb)
- Katsuhiro OGAWA
@ -181,16 +183,18 @@ Symfony is the result of the work of many people who made the code better
- Jeremy Livingston (jeremylivingston)
- Nikita Konstantinov
- Wodor Wodorski
- Matthieu Auger (matthieuauger)
- julien pauli (jpauli)
- Beau Simensen (simensen)
- Robert Kiss (kepten)
- John Kary (johnkary)
- Ruben Gonzalez (rubenrua)
- Kim Hemsø Rasmussen (kimhemsoe)
- Florian Lonqueu-Brochard (florianlb)
- Tom Van Looy (tvlooy)
- Wouter Van Hecke
- Joshua Thijssen
- Peter Kruithof (pkruithof)
- Vladimir Reznichenko (kalessil)
- Michael Holm (hollo)
- Warnar Boekkooi (boekkooi)
- Marc Weistroff (futurecat)
@ -222,7 +226,6 @@ Symfony is the result of the work of many people who made the code better
- Marco Pivetta (ocramius)
- Ricard Clau (ricardclau)
- Erin Millard
- John Kary (johnkary)
- Matthew Lewinski (lewinski)
- alquerci
- Francesco Levorato
@ -232,7 +235,6 @@ Symfony is the result of the work of many people who made the code better
- Inal DJAFAR (inalgnu)
- Christian Gärtner (dagardner)
- Felix Labrecque
- Vladimir Reznichenko (kalessil)
- Yaroslav Kiliba
- Sébastien Lavoie (lavoiesl)
- Terje Bråten
@ -283,7 +285,6 @@ Symfony is the result of the work of many people who made the code better
- Brian King
- Michel Salib (michelsalib)
- geoffrey
- Matthieu Auger (matthieuauger)
- Lorenz Schori
- Jeanmonod David (jeanmonod)
- Jan Schumann
@ -309,7 +310,6 @@ Symfony is the result of the work of many people who made the code better
- Christian Schmidt
- Marcin Sikoń (marphi)
- franek (franek)
- Dariusz Ruminski
- Adam Harvey
- Diego Saint Esteben (dii3g0)
- Alex Bakhturin
@ -336,6 +336,7 @@ Symfony is the result of the work of many people who made the code better
- mmoreram
- Markus Lanthaler (lanthaler)
- Vicent Soria Durá (vicentgodella)
- Anthony Ferrara
- Ioan Negulescu
- Jakub Škvára (jskvara)
- Daniel Beyer
@ -343,9 +344,11 @@ Symfony is the result of the work of many people who made the code better
- alexpods
- Erik Trapman (eriktrapman)
- De Cock Xavier (xdecock)
- Scott Arciszewski
- Norbert Orzechowicz (norzechowicz)
- Tobias Nyholm (tobias)
- Matthijs van den Bos (matthijs)
- Loick Piera (pyrech)
- Lenard Palko
- Nils Adermann (naderman)
- Gábor Fási
@ -371,9 +374,11 @@ Symfony is the result of the work of many people who made the code better
- Zach Badgett (zachbadgett)
- Aurélien Fredouelle
- Pavel Campr (pcampr)
- Maxime Steinhausser (ogizanagi)
- Disquedur
- Geoffrey Tran (geoff)
- Jan Behrens
- Mantas Var (mvar)
- Sebastian Krebs
- Christopher Davis (chrisguitarguy)
- Thomas Lallement (raziel057)
@ -387,6 +392,7 @@ Symfony is the result of the work of many people who made the code better
- Max Rath (drak3)
- Stéphane Escandell (sescandell)
- Sinan Eldem
- Alexandre Dupuy (satchette)
- Nahuel Cuesta (ncuesta)
- Chris Boden (cboden)
- Asmir Mustafic (goetas)
@ -484,7 +490,6 @@ Symfony is the result of the work of many people who made the code better
- Xavier Lacot (xavier)
- Olivier Maisonneuve (olineuve)
- Francis Turmel (fturmel)
- Loick Piera (pyrech)
- cgonzalez
- Ben
- Jayson Xu (superjavason)
@ -506,6 +511,7 @@ Symfony is the result of the work of many people who made the code better
- Fabian Vogler (fabian)
- Korvin Szanto
- Maksim Kotlyar (makasim)
- Ivan Kurnosov
- Neil Ferreira
- Dmitry Parnas (parnas)
- DQNEO
@ -516,6 +522,7 @@ Symfony is the result of the work of many people who made the code better
- David Romaní
- Patrick Allaert
- Gustavo Falco (gfalco)
- Matt Robinson (inanimatt)
- Aleksey Podskrebyshev
- David Marín Carreño (davefx)
- Jörn Lang (j.lang)
@ -580,6 +587,7 @@ Symfony is the result of the work of many people who made the code better
- Michael Tibben
- Sander Marechal
- Radosław Benkel
- Marcos Sánchez
- ttomor
- Mei Gwilym (meigwilym)
- Michael H. Arieli (excelwebzone)
@ -622,6 +630,7 @@ Symfony is the result of the work of many people who made the code better
- nacho
- Piotr Antosik (antek88)
- Artem Lopata
- Samuel ROZE (sroze)
- Marcos Quesada (marcos_quesada)
- Matthew Vickery (mattvick)
- Dan Finnie
@ -675,6 +684,7 @@ Symfony is the result of the work of many people who made the code better
- Yannick
- Eduardo García Sanz (coma)
- Sebastian Grodzicki (sgrodzicki)
- Michael Lee (zerustech)
- Roy Van Ginneken
- David de Boer (ddeboer)
- Gilles Doge (gido)
@ -721,6 +731,7 @@ Symfony is the result of the work of many people who made the code better
- Malaney J. Hill
- Christian Flach (cmfcmf)
- Cédric Girard (enk_)
- Lars Ambrosius Wallenborn (larsborn)
- Oriol Mangas Abellan (oriolman)
- Sebastian Göttschkes (sgoettschkes)
- Tatsuya Tsuruoka
@ -847,6 +858,7 @@ Symfony is the result of the work of many people who made the code better
- Gunnar Lium (gunnarlium)
- Tiago Garcia (tiagojsag)
- Artiom
- Jakub Simon
- Bouke Haarsma
- Martin Eckhardt
- Denis Zunke
@ -876,7 +888,6 @@ Symfony is the result of the work of many people who made the code better
- Vasily Khayrulin (sirian)
- Stefan Koopmanschap (skoop)
- Stefan Hüsges (tronsha)
- Ivan Kurnosov
- stloyd
- Chris Tickner
- Andrew Coulton
@ -905,7 +916,6 @@ Symfony is the result of the work of many people who made the code better
- Julius Beckmann
- Romain Dorgueil
- Grayson Koonce (breerly)
- Matt Robinson (inanimatt)
- Karim Cassam Chenaï (ka)
- Nicolas Bastien (nicolas_bastien)
- Andy Stanberry
@ -992,6 +1002,7 @@ Symfony is the result of the work of many people who made the code better
- grifx
- Robert Campbell
- Matt Lehner
- Hidde Wieringa
- Hein Zaw Htet™
- Ruben Kruiswijk
- Michael J
@ -1048,9 +1059,7 @@ Symfony is the result of the work of many people who made the code better
- Muriel (metalmumu)
- Michaël Perrin (michael.perrin)
- Michael Pohlers (mick_the_big)
- Mantas Var (mvar)
- Cayetano Soriano Gallego (neoshadybeat)
- Maxime Steinhausser (ogizanagi)
- Pablo Monterde Perez (plebs)
- Jimmy Leger (redpanda)
- Cyrille Jouineau (tuxosaurus)
@ -1128,6 +1137,7 @@ Symfony is the result of the work of many people who made the code better
- Brian Freytag
- Skorney
- mieszko4
- Neophy7e
- Arrilot
- Markus Staab
- Pierre-Louis LAUNAY
@ -1140,6 +1150,8 @@ Symfony is the result of the work of many people who made the code better
- Sema
- Thorsten Hallwas
- Michael Squires
- Norman Soetbeer
- Benjamin Long
- Matt Janssen
- Peter Gribanov
- kwiateusz
@ -1235,6 +1247,7 @@ Symfony is the result of the work of many people who made the code better
- Vincent (vincent1870)
- Eugene Babushkin (warl)
- Xavier Amado (xamado)
- Jesper Søndergaard Pedersen (zerrvox)
- Florent Cailhol
- szymek
- craigmarvelley

View File

@ -15,7 +15,7 @@ Router
`foo%bar%2` which would be compiled to `$foo % $bar % 2` in 2.6
but in 2.7 you would get an error if `bar` parameter
doesn't exist or unexpected result otherwise.
Form
----
@ -23,41 +23,446 @@ Form
AbstractType or AbstractExtensionType has been deprecated in favor of
overriding the new "configureOptions" method.
The method "setDefaultOptions(OptionsResolverInterface $resolver)" will
The method "setDefaultOptions(OptionsResolverInterface $resolver)" will
be renamed in Symfony 3.0 to "configureOptions(OptionsResolver $resolver)".
Before:
```php
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class TaskType extends AbstractType
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class TaskType extends AbstractType
{
// ...
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
// ...
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Task',
));
}
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Task',
));
}
}
```
After:
```php
use Symfony\Component\OptionsResolver\OptionsResolver;
class TaskType extends AbstractType
use Symfony\Component\OptionsResolver\OptionsResolver;
class TaskType extends AbstractType
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
// ...
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Task',
));
}
$resolver->setDefaults(array(
'data_class' => 'AppBundle\Entity\Task',
));
}
}
```
* The "choice_list" option of ChoiceType was deprecated. You should use
"choices_as_values" or "choice_loader" now.
Before:
```php
$form->add('status', 'choice', array(
'choice_list' => new ObjectChoiceList(array(
Status::getInstance(Status::ENABLED),
Status::getInstance(Status::DISABLED),
Status::getInstance(Status::IGNORED),
)),
));
```
After:
```php
$form->add('status', 'choice', array(
'choices' => array(
Status::getInstance(Status::ENABLED),
Status::getInstance(Status::DISABLED),
Status::getInstance(Status::IGNORED),
),
'choices_as_values' => true,
));
```
* You should flip the keys and values of the "choices" option in ChoiceType
and set the "choices_as_values" option to `true`. The default value of that
option will be switched to `true` in Symfony 3.0.
Before:
```php
$form->add('status', 'choice', array(
'choices' => array(
Status::ENABLED => 'Enabled',
Status::DISABLED => 'Disabled',
Status::IGNORED => 'Ignored',
)),
));
```
After:
```php
$form->add('status', 'choice', array(
'choices' => array(
'Enabled' => Status::ENABLED,
'Disabled' => Status::DISABLED,
'Ignored' => Status::IGNORED,
),
'choices_as_values' => true,
));
```
* `Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface` was
deprecated and will be removed in Symfony 3.0. You should use
`Symfony\Component\Form\ChoiceList\ChoiceListInterface` instead.
Before:
```php
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface;
public function doSomething(ChoiceListInterface $choiceList)
{
// ...
}
```
After:
```php
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
public function doSomething(ChoiceListInterface $choiceList)
{
// ...
}
```
* `Symfony\Component\Form\Extension\Core\ChoiceList\View\ChoiceView` was
deprecated and will be removed in Symfony 3.0. You should use
`Symfony\Component\Form\ChoiceList\View\ChoiceView` instead.
Note that the order of the arguments passed to the constructor was inverted.
Before:
```php
use Symfony\Component\Form\Extension\Core\ChoiceList\View\ChoiceView;
$view = new ChoiceView($data, 'value', 'Label');
```
After:
```php
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
$view = new ChoiceView('Label', 'value', $data);
```
* `Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList` was
deprecated and will be removed in Symfony 3.0. You should use
`Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory` instead.
Before:
```php
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceList;
$choiceList = new ChoiceList(
array(Status::ENABLED, Status::DISABLED, Status::IGNORED),
array('Enabled', 'Disabled', 'Ignored'),
// Preferred choices
array(Status::ENABLED),
);
```
After:
```php
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
$factory = new DefaultChoiceListFactory();
$choices = array(Status::ENABLED, Status::DISABLED, Status::IGNORED);
$labels = array('Enabled', 'Disabled', 'Ignored');
$choiceList = $factory->createListFromChoices($choices);
$choiceListView = $factory->createView(
$choiceList,
// Preferred choices
array(Status::ENABLED),
// Labels
function ($choice, $key) use ($labels) {
return $labels[$key];
}
);
```
* `Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList` was
deprecated and will be removed in Symfony 3.0. You should use
`Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory::createListFromLoader()`
together with an implementation of
`Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface` instead.
Before:
```php
use Symfony\Component\Form\Extension\Core\ChoiceList\LazyChoiceList;
class MyLazyChoiceList extends LazyChoiceList
{
public function loadChoiceList()
{
// load $choiceList
return $choiceList;
}
}
$choiceList = new MyLazyChoiceList();
```
After:
```php
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
class MyChoiceLoader implements ChoiceLoaderInterface
{
// ...
}
$factory = new DefaultChoiceListFactory();
$choiceList = $factory->createListFromLoader(new MyChoiceLoader());
```
* `Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList` was
deprecated and will be removed in Symfony 3.0. You should use
`Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory` instead.
Before:
```php
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
$choiceList = new ObjectChoiceList(
array(Status::getInstance(Status::ENABLED), Status::getInstance(Status::DISABLED)),
// Label property
'name'
);
```
After:
```php
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
$factory = new DefaultChoiceListFactory();
$choiceList = $factory->createListFromChoices(array(
Status::getInstance(Status::ENABLED),
Status::getInstance(Status::DISABLED),
));
$choiceListView = $factory->createView(
$choiceList,
// Preferred choices
array(),
// Label property
'name'
);
```
* `Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList` was
deprecated and will be removed in Symfony 3.0. You should use
`Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory` instead.
Before:
```php
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
$choiceList = new SimpleChoiceList(array(
Status::ENABLED => 'Enabled',
Status::DISABLED => 'Disabled',
));
```
After:
```php
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
$factory = new DefaultChoiceListFactory();
$choices = array(Status::ENABLED, Status::DISABLED);
$labels = array('Enabled', 'Disabled');
$choiceList = $factory->createListFromChoices($choices);
$choiceListView = $factory->createView(
$choiceList,
// Preferred choices
array(),
// Label
function ($choice, $key) use ($labels) {
return $labels[$key];
}
);
```
* The "property" option of `DoctrineType` was deprecated. You should use the
new inherited option "choice_label" instead, which has the same effect.
Before:
```php
$form->add('tags', 'entity', array(
'class' => 'Acme\Entity\MyTag',
'property' => 'name',
))
```
After:
```php
$form->add('tags', 'entity', array(
'class' => 'Acme\Entity\MyTag',
'choice_label' => 'name',
))
```
* The "loader" option of `DoctrineType` was deprecated and will be removed in
Symfony 3.0. You should override the `getLoader()` method instead in a custom
type.
Before:
```php
$form->add('tags', 'entity', array(
'class' => 'Acme\Entity\MyTag',
'loader' => new MyEntityLoader(),
))
```
After:
class MyEntityType extends DoctrineType
{
// ...
public function getLoader()
{
return new MyEntityLoader();
}
}
* `Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList` was
deprecated and will be removed in Symfony 3.0. You should use
`Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader` instead.
Before:
```php
use Symfony\Component\Form\Extension\Core\ChoiceList\SimpleChoiceList;
$choiceList = new EntityChoiceList($em, 'Acme\Entity\MyEntity');
```
After:
```php
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
$factory = new DefaultChoiceListFactory();
$choices = array(Status::ENABLED, Status::DISABLED);
$labels = array('Enabled', 'Disabled');
$choiceLoader = new DoctrineChoiceLoader($factory, $em, 'Acme\Entity\MyEntity');
$choiceList = $factory->createListFromLoader($choiceLoader);
```
* Passing a query builder closure to `ORMQueryBuilderLoader` was deprecated and
will not be supported anymore in Symfony 3.0. You should pass resolved query
builders only.
Consequently, the arguments `$manager` and `$class` of `ORMQueryBuilderLoader`
have been deprecated as well.
Note that the "query_builder" option of `DoctrineType` *does* support
closures, but the closure is now resolved in the type instead of in the
loader.
Before:
```
use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
$queryBuilder = function () {
// return QueryBuilder
};
$loader = new ORMQueryBuilderLoader($queryBuilder);
```
After:
```
use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
// create $queryBuilder
$loader = new ORMQueryBuilderLoader($queryBuilder);
```
* The classes `ChoiceToBooleanArrayTransformer`,
`ChoicesToBooleanArrayTransformer`, `FixRadioInputListener` and
`FixCheckboxInputListener` were deprecated and will be removed in Symfony 3.0.
Their functionality is covered by the new classes `RadioListMapper` and
`CheckboxListMapper`.
* The ability to translate Doctrine type entries by the translator component
is now disabled by default and to enable it you must explicitly set the option
"choice_translation_domain" to true
Before:
```
$form->add('products', 'entity', array(
'class' => 'AppBundle/Entity/Product',
));
```
After:
```
$form->add('products', 'entity', array(
'class' => 'AppBundle/Entity/Product',
'choice_translation_domain' => true,
));
```
* In the block `choice_widget_options` the `translation_domain` has been replaced
with the `choice_translation_domain` option.
Before:
```jinja
{{ choice.label|trans({}, translation_domain) }}
```
After:
```jinja
{{ choice_translation_domain is sameas(false) ? choice.label : choice.label|trans({}, choice_translation_domain) }}
```
Serializer

View File

@ -110,6 +110,24 @@ UPGRADE FROM 2.x to 3.0
### Form
* The option "precision" was renamed to "scale".
Before:
```php
$builder->add('length', 'number', array(
'precision' => 3,
));
```
After:
```php
$builder->add('length', 'number', array(
'scale' => 3,
));
```
* The method `AbstractType::setDefaultOptions(OptionsResolverInterface $resolver)` and
`AbstractTypeExtension::setDefaultOptions(OptionsResolverInterface $resolver)` have been
renamed. You should use `AbstractType::configureOptions(OptionsResolver $resolver)` and

View File

@ -1,6 +1,16 @@
CHANGELOG
=========
2.7.0
-----
* added DoctrineChoiceLoader
* deprecated EntityChoiceList
* deprecated passing a query builder closure to ORMQueryBuilderLoader
* deprecated $manager and $em arguments of ORMQueryBuilderLoader
* added optional arguments $propertyAccessor and $choiceListFactory to DoctrineOrmExtension constructor
* deprecated "loader" and "property" options of DoctrineType
2.4.0
-----

View File

@ -0,0 +1,173 @@
<?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\Doctrine\Form\ChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
/**
* Loads choices using a Doctrine object manager.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class DoctrineChoiceLoader implements ChoiceLoaderInterface
{
/**
* @var ChoiceListFactoryInterface
*/
private $factory;
/**
* @var ObjectManager
*/
private $manager;
/**
* @var string
*/
private $class;
/**
* @var IdReader
*/
private $idReader;
/**
* @var null|EntityLoaderInterface
*/
private $objectLoader;
/**
* @var ChoiceListInterface
*/
private $choiceList;
/**
* Creates a new choice loader.
*
* Optionally, an implementation of {@link EntityLoaderInterface} can be
* passed which optimizes the object loading for one of the Doctrine
* mapper implementations.
*
* @param ChoiceListFactoryInterface $factory The factory for creating
* the loaded choice list
* @param ObjectManager $manager The object manager
* @param string $class The class name of the
* loaded objects
* @param IdReader $idReader The reader for the object
* IDs.
* @param null|EntityLoaderInterface $objectLoader The objects loader
*/
public function __construct(ChoiceListFactoryInterface $factory, ObjectManager $manager, $class, IdReader $idReader = null, EntityLoaderInterface $objectLoader = null)
{
$classMetadata = $manager->getClassMetadata($class);
$this->factory = $factory;
$this->manager = $manager;
$this->class = $classMetadata->getName();
$this->idReader = $idReader ?: new IdReader($manager, $classMetadata);
$this->objectLoader = $objectLoader;
}
/**
* {@inheritdoc}
*/
public function loadChoiceList($value = null)
{
if ($this->choiceList) {
return $this->choiceList;
}
$objects = $this->objectLoader
? $this->objectLoader->getEntities()
: $this->manager->getRepository($this->class)->findAll();
$this->choiceList = $this->factory->createListFromChoices($objects, $value);
return $this->choiceList;
}
/**
* {@inheritdoc}
*/
public function loadValuesForChoices(array $choices, $value = null)
{
// Performance optimization
if (empty($choices)) {
return array();
}
// Optimize performance for single-field identifiers. We already
// know that the IDs are used as values
// Attention: This optimization does not check choices for existence
if (!$this->choiceList && $this->idReader->isSingleId()) {
$values = array();
// Maintain order and indices of the given objects
foreach ($choices as $i => $object) {
if ($object instanceof $this->class) {
// Make sure to convert to the right format
$values[$i] = (string) $this->idReader->getIdValue($object);
}
}
return $values;
}
return $this->loadChoiceList($value)->getValuesForChoices($choices);
}
/**
* {@inheritdoc}
*/
public function loadChoicesForValues(array $values, $value = null)
{
// Performance optimization
// Also prevents the generation of "WHERE id IN ()" queries through the
// object loader. At least with MySQL and on the development machine
// this was tested on, no exception was thrown for such invalid
// statements, consequently no test fails when this code is removed.
// https://github.com/symfony/symfony/pull/8981#issuecomment-24230557
if (empty($values)) {
return array();
}
// Optimize performance in case we have an object loader and
// a single-field identifier
if (!$this->choiceList && $this->objectLoader && $this->idReader->isSingleId()) {
$unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $values);
$objectsById = array();
$objects = array();
// Maintain order and indices from the given $values
// An alternative approach to the following loop is to add the
// "INDEX BY" clause to the Doctrine query in the loader,
// but I'm not sure whether that's doable in a generic fashion.
foreach ($unorderedObjects as $object) {
$objectsById[$this->idReader->getIdValue($object)] = $object;
}
foreach ($values as $i => $id) {
if (isset($objectsById[$id])) {
$objects[$i] = $objectsById[$id];
}
}
return $objects;
}
return $this->loadChoiceList($value)->getChoicesForValues($values);
}
}

View File

@ -11,17 +11,20 @@
namespace Symfony\Bridge\Doctrine\Form\ChoiceList;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Component\Form\Exception\StringCastException;
use Symfony\Component\Form\Extension\Core\ChoiceList\ObjectChoiceList;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
/**
* A choice list presenting a list of Doctrine entities as choices.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
* Use {@link DoctrineChoiceLoader} instead.
*/
class EntityChoiceList extends ObjectChoiceList
{
@ -126,6 +129,8 @@ class EntityChoiceList extends ObjectChoiceList
}
parent::__construct($entities, $labelPath, $preferredEntities, $groupPath, null, $propertyAccessor);
trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader instead.', E_USER_DEPRECATED);
}
/**

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\Bridge\Doctrine\Form\ChoiceList;
use Doctrine\Common\Persistence\Mapping\ClassMetadata;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\Exception\RuntimeException;
/**
* A utility for reading object IDs.
*
* @since 1.0
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @internal This class is meant for internal use only.
*/
class IdReader
{
/**
* @var ObjectManager
*/
private $om;
/**
* @var ClassMetadata
*/
private $classMetadata;
/**
* @var bool
*/
private $singleId;
/**
* @var bool
*/
private $intId;
/**
* @var string
*/
private $idField;
public function __construct(ObjectManager $om, ClassMetadata $classMetadata)
{
$ids = $classMetadata->getIdentifierFieldNames();
$idType = $classMetadata->getTypeOfField(current($ids));
$this->om = $om;
$this->classMetadata = $classMetadata;
$this->singleId = 1 === count($ids);
$this->intId = $this->singleId && in_array($idType, array('integer', 'smallint', 'bigint'));
$this->idField = current($ids);
}
/**
* Returns whether the class has a single-column ID.
*
* @return bool Returns `true` if the class has a single-column ID and
* `false` otherwise.
*/
public function isSingleId()
{
return $this->singleId;
}
/**
* Returns whether the class has a single-column integer ID.
*
* @return bool Returns `true` if the class has a single-column integer ID
* and `false` otherwise.
*/
public function isIntId()
{
return $this->intId;
}
/**
* Returns the ID value for an object.
*
* This method assumes that the object has a single-column ID.
*
* @param object $object The object.
*
* @return mixed The ID value.
*/
public function getIdValue($object)
{
if (!$object) {
return;
}
if (!$this->om->contains($object)) {
throw new RuntimeException(
'Entities passed to the choice field must be managed. Maybe '.
'persist them in the entity manager?'
);
}
$this->om->initializeObject($object);
return current($this->classMetadata->getIdentifierValues($object));
}
/**
* Returns the name of the ID field.
*
* This method assumes that the object has a single-column ID.
*
* @return string The name of the ID field.
*/
public function getIdField()
{
return $this->idField;
}
}

View File

@ -17,7 +17,10 @@ use Doctrine\DBAL\Connection;
use Doctrine\ORM\EntityManager;
/**
* Getting Entities through the ORM QueryBuilder.
* Loads entities using a {@link QueryBuilder} instance.
*
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ORMQueryBuilderLoader implements EntityLoaderInterface
{
@ -34,9 +37,14 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface
/**
* Construct an ORM Query Builder Loader.
*
* @param QueryBuilder|\Closure $queryBuilder
* @param EntityManager $manager
* @param string $class
* @param QueryBuilder|\Closure $queryBuilder The query builder or a closure
* for creating the query builder.
* Passing a closure is
* deprecated and will not be
* supported anymore as of
* Symfony 3.0.
* @param EntityManager $manager Deprecated.
* @param string $class Deprecated.
*
* @throws UnexpectedTypeException
*/
@ -49,10 +57,15 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface
}
if ($queryBuilder instanceof \Closure) {
trigger_error('Passing a QueryBuilder closure to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED);
if (!$manager instanceof EntityManager) {
throw new UnexpectedTypeException($manager, 'Doctrine\ORM\EntityManager');
}
trigger_error('Passing an EntityManager to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED);
trigger_error('Passing a class to '.__CLASS__.'::__construct() is deprecated since version 2.7 and will be removed in 3.0.', E_USER_DEPRECATED);
$queryBuilder = $queryBuilder($manager->getRepository($class));
if (!$queryBuilder instanceof QueryBuilder) {

View File

@ -14,21 +14,38 @@ namespace Symfony\Bridge\Doctrine\Form;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractExtension;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
class DoctrineOrmExtension extends AbstractExtension
{
protected $registry;
public function __construct(ManagerRegistry $registry)
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* @var ChoiceListFactoryInterface
*/
private $choiceListFactory;
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null)
{
$this->registry = $registry;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->choiceListFactory = $choiceListFactory ?: new CachingFactoryDecorator(new PropertyAccessDecorator(new DefaultChoiceListFactory(), $this->propertyAccessor));
}
protected function loadTypes()
{
return array(
new EntityType($this->registry, PropertyAccess::createPropertyAccessor()),
new EntityType($this->registry, $this->propertyAccessor, $this->choiceListFactory),
);
}

View File

@ -12,17 +12,23 @@
namespace Symfony\Bridge\Doctrine\Form\Type;
use Doctrine\Common\Persistence\ManagerRegistry;
use Symfony\Component\Form\Exception\RuntimeException;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader;
use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface;
use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader;
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Factory\DefaultChoiceListFactory;
use Symfony\Component\Form\ChoiceList\Factory\PropertyAccessDecorator;
use Symfony\Component\Form\Exception\RuntimeException;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
abstract class DoctrineType extends AbstractType
@ -33,19 +39,63 @@ abstract class DoctrineType extends AbstractType
protected $registry;
/**
* @var array
* @var ChoiceListFactoryInterface
*/
private $choiceListCache = array();
private $choiceListFactory;
/**
* @var PropertyAccessorInterface
* @var IdReader[]
*/
private $propertyAccessor;
private $idReaders = array();
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null)
/**
* @var DoctrineChoiceLoader[]
*/
private $choiceLoaders = array();
/**
* Creates the label for a choice.
*
* For backwards compatibility, objects are cast to strings by default.
*
* @param object $choice The object.
*
* @return string The string representation of the object.
*
* @internal This method is public to be usable as callback. It should not
* be used in user code.
*/
public static function createChoiceLabel($choice)
{
return (string) $choice;
}
/**
* Creates the field name for a choice.
*
* This method is used to generate field names if the underlying object has
* a single-column integer ID. In that case, the value of the field is
* the ID of the object. That ID is also used as field name.
*
* @param object $choice The object.
* @param int|string $key The choice key.
* @param string $value The choice value. Corresponds to the object's
* ID here.
*
* @return string The field name.
*
* @internal This method is public to be usable as callback. It should not
* be used in user code.
*/
public static function createChoiceName($choice, $key, $value)
{
return (string) $value;
}
public function __construct(ManagerRegistry $registry, PropertyAccessorInterface $propertyAccessor = null, ChoiceListFactoryInterface $choiceListFactory = null)
{
$this->registry = $registry;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
$this->choiceListFactory = $choiceListFactory ?: new PropertyAccessDecorator(new DefaultChoiceListFactory(), $propertyAccessor);
}
public function buildForm(FormBuilderInterface $builder, array $options)
@ -60,85 +110,96 @@ abstract class DoctrineType extends AbstractType
public function configureOptions(OptionsResolver $resolver)
{
$choiceListCache = &$this->choiceListCache;
$registry = $this->registry;
$propertyAccessor = $this->propertyAccessor;
$choiceListFactory = $this->choiceListFactory;
$idReaders = &$this->idReaders;
$choiceLoaders = &$this->choiceLoaders;
$loader = function (Options $options) {
$queryBuilder = (null !== $options['query_builder'])
? $options['query_builder']
: $options['em']->getRepository($options['class'])->createQueryBuilder('e');
$choiceLoader = function (Options $options) use ($choiceListFactory, &$choiceLoaders) {
// This closure and the "query_builder" options should be pushed to
// EntityType in Symfony 3.0 as they are specific to the ORM
return $this->getLoader($options['em'], $queryBuilder, $options['class']);
};
// Unless the choices are given explicitly, load them on demand
if (null === $options['choices']) {
// We consider two query builders with an equal SQL string and
// equal parameters to be equal
$qbParts = $options['query_builder']
? array(
$options['query_builder']->getQuery()->getSQL(),
$options['query_builder']->getParameters()->toArray(),
)
: null;
$choiceList = function (Options $options) use (&$choiceListCache, $propertyAccessor) {
// Support for closures
$propertyHash = is_object($options['property'])
? spl_object_hash($options['property'])
: $options['property'];
$choiceHashes = $options['choices'];
// Support for recursive arrays
if (is_array($choiceHashes)) {
// A second parameter ($key) is passed, so we cannot use
// spl_object_hash() directly (which strictly requires
// one parameter)
array_walk_recursive($choiceHashes, function (&$value) {
$value = spl_object_hash($value);
});
} elseif ($choiceHashes instanceof \Traversable) {
$hashes = array();
foreach ($choiceHashes as $value) {
$hashes[] = spl_object_hash($value);
}
$choiceHashes = $hashes;
}
$preferredChoiceHashes = $options['preferred_choices'];
if (is_array($preferredChoiceHashes)) {
array_walk_recursive($preferredChoiceHashes, function (&$value) {
$value = spl_object_hash($value);
});
}
// Support for custom loaders (with query builders)
$loaderHash = is_object($options['loader'])
? spl_object_hash($options['loader'])
: $options['loader'];
// Support for closures
$groupByHash = is_object($options['group_by'])
? spl_object_hash($options['group_by'])
: $options['group_by'];
$hash = hash('sha256', json_encode(array(
spl_object_hash($options['em']),
$options['class'],
$propertyHash,
$loaderHash,
$choiceHashes,
$preferredChoiceHashes,
$groupByHash,
)));
if (!isset($choiceListCache[$hash])) {
$choiceListCache[$hash] = new EntityChoiceList(
$hash = CachingFactoryDecorator::generateHash(array(
$options['em'],
$options['class'],
$options['property'],
$qbParts,
$options['loader'],
$options['choices'],
$options['preferred_choices'],
$options['group_by'],
$propertyAccessor
));
if (isset($choiceLoaders[$hash])) {
return $choiceLoaders[$hash];
}
if ($options['loader']) {
$entityLoader = $options['loader'];
} elseif (null !== $options['query_builder']) {
$entityLoader = $this->getLoader($options['em'], $options['query_builder'], $options['class']);
} else {
$queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e');
$entityLoader = $this->getLoader($options['em'], $queryBuilder, $options['class']);
}
$choiceLoaders[$hash] = new DoctrineChoiceLoader(
$choiceListFactory,
$options['em'],
$options['class'],
$options['id_reader'],
$entityLoader
);
return $choiceLoaders[$hash];
}
};
$choiceLabel = function (Options $options) {
// BC with the "property" option
if ($options['property']) {
return $options['property'];
}
return $choiceListCache[$hash];
// BC: use __toString() by default
return array(__CLASS__, 'createChoiceLabel');
};
$choiceName = function (Options $options) {
/** @var IdReader $idReader */
$idReader = $options['id_reader'];
// If the object has a single-column, numeric ID, use that ID as
// field name. We can only use numeric IDs as names, as we cannot
// guarantee that a non-numeric ID contains a valid form name
if ($idReader->isIntId()) {
return array(__CLASS__, 'createChoiceName');
}
// Otherwise, an incrementing integer is used as name automatically
};
// The choices are always indexed by ID (see "choices" normalizer
// and DoctrineChoiceLoader), unless the ID is composite. Then they
// are indexed by an incrementing integer.
// Use the ID/incrementing integer as choice value.
$choiceValue = function (Options $options) {
/** @var IdReader $idReader */
$idReader = $options['id_reader'];
// If the entity has a single-column ID, use that ID as value
if ($idReader->isSingleId()) {
return array($idReader, 'getIdValue');
}
// Otherwise, an incrementing integer is used as value automatically
};
$emNormalizer = function (Options $options, $em) use ($registry) {
@ -164,22 +225,86 @@ abstract class DoctrineType extends AbstractType
return $em;
};
// deprecation note
$propertyNormalizer = function (Options $options, $propertyName) {
if ($propertyName) {
trigger_error('The "property" option is deprecated since version 2.7 and will be removed in 3.0. Use "choice_label" instead.', E_USER_DEPRECATED);
}
return $propertyName;
};
// Invoke the query builder closure so that we can cache choice lists
// for equal query builders
$queryBuilderNormalizer = function (Options $options, $queryBuilder) {
if (is_callable($queryBuilder)) {
$queryBuilder = call_user_func($queryBuilder, $options['em']->getRepository($options['class']));
if (!$queryBuilder instanceof QueryBuilder) {
throw new UnexpectedTypeException($queryBuilder, 'Doctrine\ORM\QueryBuilder');
}
}
return $queryBuilder;
};
// deprecation note
$loaderNormalizer = function (Options $options, $loader) {
if ($loader) {
trigger_error('The "loader" option is deprecated since version 2.7 and will be removed in 3.0. Override getLoader() instead.', E_USER_DEPRECATED);
}
return $loader;
};
// Set the "id_reader" option via the normalizer. This option is not
// supposed to be set by the user.
$idReaderNormalizer = function (Options $options) use (&$idReaders) {
$hash = CachingFactoryDecorator::generateHash(array(
$options['em'],
$options['class'],
));
// The ID reader is a utility that is needed to read the object IDs
// when generating the field values. The callback generating the
// field values has no access to the object manager or the class
// of the field, so we store that information in the reader.
// The reader is cached so that two choice lists for the same class
// (and hence with the same reader) can successfully be cached.
if (!isset($idReaders[$hash])) {
$classMetadata = $options['em']->getClassMetadata($options['class']);
$idReaders[$hash] = new IdReader($options['em'], $classMetadata);
}
return $idReaders[$hash];
};
$resolver->setDefaults(array(
'em' => null,
'property' => null,
'property' => null, // deprecated, use "choice_label"
'query_builder' => null,
'loader' => $loader,
'loader' => null, // deprecated, use "choice_loader"
'choices' => null,
'choice_list' => $choiceList,
'group_by' => null,
'choices_as_values' => true,
'choice_loader' => $choiceLoader,
'choice_label' => $choiceLabel,
'choice_name' => $choiceName,
'choice_value' => $choiceValue,
'id_reader' => null, // internal
'choice_translation_domain' => false,
));
$resolver->setRequired(array('class'));
$resolver->setNormalizer('em', $emNormalizer);
$resolver->setNormalizer('property', $propertyNormalizer);
$resolver->setNormalizer('query_builder', $queryBuilderNormalizer);
$resolver->setNormalizer('loader', $loaderNormalizer);
$resolver->setNormalizer('id_reader', $idReaderNormalizer);
$resolver->setAllowedTypes('em', array('null', 'string', 'Doctrine\Common\Persistence\ObjectManager'));
$resolver->setAllowedTypes('loader', array('null', 'Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface'));
$resolver->setAllowedTypes('query_builder', array('null', 'callable', 'Doctrine\ORM\QueryBuilder'));
}
/**

View File

@ -17,71 +17,18 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
class EntityType extends DoctrineType
{
/**
* @var ORMQueryBuilderLoader[]
*/
private $loaderCache = array();
/**
* Return the default loader object.
*
* @param ObjectManager $manager
* @param mixed $queryBuilder
* @param QueryBuilder $queryBuilder
* @param string $class
*
* @return ORMQueryBuilderLoader
*/
public function getLoader(ObjectManager $manager, $queryBuilder, $class)
{
if (!$queryBuilder instanceof QueryBuilder) {
return new ORMQueryBuilderLoader(
$queryBuilder,
$manager,
$class
);
}
$queryBuilderHash = $this->getQueryBuilderHash($queryBuilder);
$loaderHash = $this->getLoaderHash($manager, $queryBuilderHash, $class);
if (!isset($this->loaderCache[$loaderHash])) {
$this->loaderCache[$loaderHash] = new ORMQueryBuilderLoader(
$queryBuilder,
$manager,
$class
);
}
return $this->loaderCache[$loaderHash];
}
/**
* @param QueryBuilder $queryBuilder
*
* @return string
*/
private function getQueryBuilderHash(QueryBuilder $queryBuilder)
{
return hash('sha256', json_encode(array(
'sql' => $queryBuilder->getQuery()->getSQL(),
'parameters' => $queryBuilder->getParameters(),
)));
}
/**
* @param ObjectManager $manager
* @param string $queryBuilderHash
* @param string $class
*
* @return string
*/
private function getLoaderHash(ObjectManager $manager, $queryBuilderHash, $class)
{
return hash('sha256', json_encode(array(
'manager' => spl_object_hash($manager),
'queryBuilder' => $queryBuilderHash,
'class' => $class,
)));
return new ORMQueryBuilderLoader($queryBuilder, $manager, $class);
}
public function getName()

View File

@ -16,6 +16,9 @@ use Symfony\Component\DependencyInjection\Container;
class ContainerAwareEventManagerTest extends \PHPUnit_Framework_TestCase
{
private $container;
private $evm;
protected function setUp()
{
$this->container = new Container();

View File

@ -19,6 +19,9 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityChoiceList;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Doctrine\ORM\Tools\SchemaTool;
/**
* @group legacy
*/
class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase
{
const SINGLE_INT_ID_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity';
@ -36,6 +39,8 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase
protected function setUp()
{
$this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED);
$this->em = DoctrineTestHelper::createTestEntityManager();
$schemaTool = new SchemaTool($this->em);
@ -70,7 +75,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase
* @expectedException \Symfony\Component\Form\Exception\StringCastException
* @expectedMessage Entity "Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity" passed to the choice field must have a "__toString()" method defined (or you can also override the "property" option).
*/
public function testEntitiesMustHaveAToStringMethod()
public function testLegacyEntitiesMustHaveAToStringMethod()
{
$entity1 = new SingleIntIdNoToStringEntity(1, 'Foo');
$entity2 = new SingleIntIdNoToStringEntity(2, 'Bar');
@ -96,7 +101,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase
/**
* @expectedException \Symfony\Component\Form\Exception\RuntimeException
*/
public function testChoicesMustBeManaged()
public function testLegacyChoicesMustBeManaged()
{
$entity1 = new SingleIntIdEntity(1, 'Foo');
$entity2 = new SingleIntIdEntity(2, 'Bar');
@ -118,7 +123,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase
$choiceList->getChoices();
}
public function testInitExplicitChoices()
public function testLegacyInitExplicitChoices()
{
$entity1 = new SingleIntIdEntity(1, 'Foo');
$entity2 = new SingleIntIdEntity(2, 'Bar');
@ -141,7 +146,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase
$this->assertSame(array(1 => $entity1, 2 => $entity2), $choiceList->getChoices());
}
public function testInitEmptyChoices()
public function testLegacyInitEmptyChoices()
{
$entity1 = new SingleIntIdEntity(1, 'Foo');
$entity2 = new SingleIntIdEntity(2, 'Bar');
@ -161,7 +166,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase
$this->assertSame(array(), $choiceList->getChoices());
}
public function testInitNestedChoices()
public function testLegacyInitNestedChoices()
{
$entity1 = new SingleIntIdEntity(1, 'Foo');
$entity2 = new SingleIntIdEntity(2, 'Bar');
@ -189,7 +194,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase
), $choiceList->getRemainingViews());
}
public function testGroupByPropertyPath()
public function testLegacyGroupByPropertyPath()
{
$item1 = new GroupableEntity(1, 'Foo', 'Group1');
$item2 = new GroupableEntity(2, 'Bar', 'Group1');
@ -224,7 +229,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase
), $choiceList->getRemainingViews());
}
public function testGroupByInvalidPropertyPathReturnsFlatChoices()
public function testLegacyGroupByInvalidPropertyPathReturnsFlatChoices()
{
$item1 = new GroupableEntity(1, 'Foo', 'Group1');
$item2 = new GroupableEntity(2, 'Bar', 'Group1');
@ -251,7 +256,7 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase
), $choiceList->getChoices());
}
public function testInitShorthandEntityName()
public function testLegacyInitShorthandEntityName()
{
$item1 = new SingleIntIdEntity(1, 'Foo');
$item2 = new SingleIntIdEntity(2, 'Bar');
@ -267,13 +272,8 @@ class GenericEntityChoiceListTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(array(1, 2), $choiceList->getValuesForChoices(array($item1, $item2)));
}
/**
* @group legacy
*/
public function testLegacyInitShorthandEntityName()
public function testLegacyInitShorthandEntityName2()
{
$this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED);
$item1 = new SingleIntIdEntity(1, 'Foo');
$item2 = new SingleIntIdEntity(2, 'Bar');

View File

@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
* @group legacy
*/
class LoadedEntityChoiceListCompositeIdTest extends AbstractEntityChoiceListCompositeIdTest
{

View File

@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
* @group legacy
*/
class LoadedEntityChoiceListSingleIntIdTest extends AbstractEntityChoiceListSingleIntIdTest
{

View File

@ -13,6 +13,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
* @group legacy
*/
class LoadedEntityChoiceListSingleStringIdTest extends AbstractEntityChoiceListSingleStringIdTest
{

View File

@ -13,10 +13,11 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
* @group legacy
*/
class UnloadedEntityChoiceListCompositeIdTest extends AbstractEntityChoiceListCompositeIdTest
{
public function testGetIndicesForValuesIgnoresNonExistingValues()
public function testLegacyGetIndicesForValuesIgnoresNonExistingValues()
{
$this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.');
}

View File

@ -16,6 +16,7 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
* @group legacy
*/
class UnloadedEntityChoiceListCompositeIdWithQueryBuilderTest extends UnloadedEntityChoiceListCompositeIdTest
{

View File

@ -13,17 +13,10 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
* @group legacy
*/
class UnloadedEntityChoiceListSingleIntIdTest extends AbstractEntityChoiceListSingleIntIdTest
{
public function testGetIndicesForValuesIgnoresNonExistingValues()
{
$this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.');
}
/**
* @group legacy
*/
public function testLegacyGetIndicesForValuesIgnoresNonExistingValues()
{
$this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.');

View File

@ -16,6 +16,7 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
* @group legacy
*/
class UnloadedEntityChoiceListSingleIntIdWithQueryBuilderTest extends UnloadedEntityChoiceListSingleIntIdTest
{

View File

@ -13,10 +13,11 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
* @group legacy
*/
class UnloadedEntityChoiceListSingleStringIdTest extends AbstractEntityChoiceListSingleStringIdTest
{
public function testGetIndicesForValuesIgnoresNonExistingValues()
public function testLegacyGetIndicesForValuesIgnoresNonExistingValues()
{
$this->markTestSkipped('Non-existing values are not detected for unloaded choice lists.');
}

View File

@ -16,6 +16,7 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader;
/**
* @author Bernhard Schussek <bschussek@gmail.com>
* @group legacy
*/
class UnloadedEntityChoiceListSingleStringIdWithQueryBuilderTest extends UnloadedEntityChoiceListSingleStringIdTest
{

View File

@ -11,21 +11,24 @@
namespace Symfony\Bridge\Doctrine\Tests\Form\Type;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Persistence\ManagerRegistry;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Tools\SchemaTool;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper;
use Symfony\Component\Form\FormBuilder;
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\GroupableEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity;
use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension;
use Doctrine\ORM\Tools\SchemaTool;
use Doctrine\Common\Collections\ArrayCollection;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\Forms;
use Symfony\Component\Form\Test\TypeTestCase;
use Symfony\Component\PropertyAccess\PropertyAccess;
class EntityTypeTest extends TypeTestCase
@ -37,12 +40,12 @@ class EntityTypeTest extends TypeTestCase
const COMPOSITE_STRING_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity';
/**
* @var \Doctrine\ORM\EntityManager
* @var EntityManager
*/
private $em;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
* @var \PHPUnit_Framework_MockObject_MockObject|ManagerRegistry
*/
private $emRegistry;
@ -128,10 +131,10 @@ class EntityTypeTest extends TypeTestCase
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'required' => false,
'property' => 'name',
'choice_label' => 'name',
));
$this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']);
$this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']);
}
public function testSetDataToUninitializedEntityWithNonRequiredToString()
@ -147,7 +150,7 @@ class EntityTypeTest extends TypeTestCase
'required' => false,
));
$this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']);
$this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']);
}
public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder()
@ -162,15 +165,15 @@ class EntityTypeTest extends TypeTestCase
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'required' => false,
'property' => 'name',
'choice_label' => 'name',
'query_builder' => $qb,
));
$this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']);
$this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']);
}
/**
* @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException
* @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException
*/
public function testConfigureQueryBuilderWithNonQueryBuilderAndNonClosure()
{
@ -249,7 +252,7 @@ class EntityTypeTest extends TypeTestCase
$field->submit(null);
$this->assertNull($field->getData());
$this->assertSame(array(), $field->getViewData());
$this->assertNull($field->getViewData());
}
public function testSubmitSingleNonExpandedNull()
@ -291,7 +294,7 @@ class EntityTypeTest extends TypeTestCase
'expanded' => false,
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'property' => 'name',
'choice_label' => 'name',
));
$field->submit('2');
@ -313,7 +316,7 @@ class EntityTypeTest extends TypeTestCase
'expanded' => false,
'em' => 'default',
'class' => self::COMPOSITE_IDENT_CLASS,
'property' => 'name',
'choice_label' => 'name',
));
// the collection key is used here
@ -337,7 +340,7 @@ class EntityTypeTest extends TypeTestCase
'expanded' => false,
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'property' => 'name',
'choice_label' => 'name',
));
$field->submit(array('1', '3'));
@ -362,7 +365,7 @@ class EntityTypeTest extends TypeTestCase
'expanded' => false,
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'property' => 'name',
'choice_label' => 'name',
));
$existing = new ArrayCollection(array(0 => $entity2));
@ -393,7 +396,7 @@ class EntityTypeTest extends TypeTestCase
'expanded' => false,
'em' => 'default',
'class' => self::COMPOSITE_IDENT_CLASS,
'property' => 'name',
'choice_label' => 'name',
));
// because of the composite key collection keys are used
@ -419,7 +422,7 @@ class EntityTypeTest extends TypeTestCase
'expanded' => false,
'em' => 'default',
'class' => self::COMPOSITE_IDENT_CLASS,
'property' => 'name',
'choice_label' => 'name',
));
$existing = new ArrayCollection(array(0 => $entity2));
@ -449,7 +452,7 @@ class EntityTypeTest extends TypeTestCase
'expanded' => true,
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'property' => 'name',
'choice_label' => 'name',
));
$field->submit('2');
@ -475,7 +478,7 @@ class EntityTypeTest extends TypeTestCase
'expanded' => true,
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'property' => 'name',
'choice_label' => 'name',
));
$field->submit(array('1', '3'));
@ -505,12 +508,12 @@ class EntityTypeTest extends TypeTestCase
'class' => self::SINGLE_IDENT_CLASS,
// not all persisted entities should be displayed
'choices' => array($entity1, $entity2),
'property' => 'name',
'choice_label' => 'name',
));
$field->submit('2');
$this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']);
$this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']);
$this->assertTrue($field->isSynchronized());
$this->assertSame($entity2, $field->getData());
$this->assertSame('2', $field->getViewData());
@ -529,7 +532,7 @@ class EntityTypeTest extends TypeTestCase
'em' => 'default',
'class' => self::ITEM_GROUP_CLASS,
'choices' => array($item1, $item2, $item3, $item4),
'property' => 'name',
'choice_label' => 'name',
'group_by' => 'groupName',
));
@ -537,9 +540,14 @@ class EntityTypeTest extends TypeTestCase
$this->assertSame('2', $field->getViewData());
$this->assertEquals(array(
'Group1' => array(1 => new ChoiceView($item1, '1', 'Foo'), 2 => new ChoiceView($item2, '2', 'Bar')),
'Group2' => array(3 => new ChoiceView($item3, '3', 'Baz')),
'4' => new ChoiceView($item4, '4', 'Boo!'),
'Group1' => new ChoiceGroupView('Group1', array(
1 => new ChoiceView('Foo', '1', $item1),
2 => new ChoiceView('Bar', '2', $item2),
)),
'Group2' => new ChoiceGroupView('Group2', array(
3 => new ChoiceView('Baz', '3', $item3),
)),
4 => new ChoiceView('Boo!', '4', $item4),
), $field->createView()->vars['choices']);
}
@ -555,11 +563,11 @@ class EntityTypeTest extends TypeTestCase
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'preferred_choices' => array($entity3, $entity2),
'property' => 'name',
'choice_label' => 'name',
));
$this->assertEquals(array(3 => new ChoiceView($entity3, '3', 'Baz'), 2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['preferred_choices']);
$this->assertEquals(array(1 => new ChoiceView($entity1, '1', 'Foo')), $field->createView()->vars['choices']);
$this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['preferred_choices']);
$this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1)), $field->createView()->vars['choices']);
}
public function testOverrideChoicesWithPreferredChoices()
@ -575,11 +583,11 @@ class EntityTypeTest extends TypeTestCase
'class' => self::SINGLE_IDENT_CLASS,
'choices' => array($entity2, $entity3),
'preferred_choices' => array($entity3),
'property' => 'name',
'choice_label' => 'name',
));
$this->assertEquals(array(3 => new ChoiceView($entity3, '3', 'Baz')), $field->createView()->vars['preferred_choices']);
$this->assertEquals(array(2 => new ChoiceView($entity2, '2', 'Bar')), $field->createView()->vars['choices']);
$this->assertEquals(array(3 => new ChoiceView('Baz', '3', $entity3)), $field->createView()->vars['preferred_choices']);
$this->assertEquals(array(2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']);
}
public function testDisallowChoicesThatAreNotIncludedChoicesSingleIdentifier()
@ -594,7 +602,7 @@ class EntityTypeTest extends TypeTestCase
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'choices' => array($entity1, $entity2),
'property' => 'name',
'choice_label' => 'name',
));
$field->submit('3');
@ -615,7 +623,7 @@ class EntityTypeTest extends TypeTestCase
'em' => 'default',
'class' => self::COMPOSITE_IDENT_CLASS,
'choices' => array($entity1, $entity2),
'property' => 'name',
'choice_label' => 'name',
));
$field->submit('2');
@ -639,7 +647,7 @@ class EntityTypeTest extends TypeTestCase
'class' => self::SINGLE_IDENT_CLASS,
'query_builder' => $repository->createQueryBuilder('e')
->where('e.id IN (1, 2)'),
'property' => 'name',
'choice_label' => 'name',
));
$field->submit('3');
@ -663,7 +671,7 @@ class EntityTypeTest extends TypeTestCase
return $repository->createQueryBuilder('e')
->where('e.id IN (1, 2)');
},
'property' => 'name',
'choice_label' => 'name',
));
$field->submit('3');
@ -687,7 +695,7 @@ class EntityTypeTest extends TypeTestCase
return $repository->createQueryBuilder('e')
->where('e.id1 IN (10, 50)');
},
'property' => 'name',
'choice_label' => 'name',
));
$field->submit('2');
@ -707,7 +715,7 @@ class EntityTypeTest extends TypeTestCase
'expanded' => false,
'em' => 'default',
'class' => self::SINGLE_STRING_IDENT_CLASS,
'property' => 'name',
'choice_label' => 'name',
));
$field->submit('foo');
@ -728,7 +736,7 @@ class EntityTypeTest extends TypeTestCase
'expanded' => false,
'em' => 'default',
'class' => self::COMPOSITE_STRING_IDENT_CLASS,
'property' => 'name',
'choice_label' => 'name',
));
// the collection key is used here
@ -752,7 +760,7 @@ class EntityTypeTest extends TypeTestCase
$this->factory->createNamed('name', 'entity', null, array(
'class' => self::SINGLE_IDENT_CLASS,
'required' => false,
'property' => 'name',
'choice_label' => 'name',
));
}
@ -767,7 +775,7 @@ class EntityTypeTest extends TypeTestCase
$this->factory->createNamed('name', 'entity', null, array(
'em' => $this->em,
'class' => self::SINGLE_IDENT_CLASS,
'property' => 'name',
'choice_label' => 'name',
));
}
@ -779,8 +787,7 @@ class EntityTypeTest extends TypeTestCase
$this->persist(array($entity1, $entity2, $entity3));
$repository = $this->em->getRepository(self::SINGLE_IDENT_CLASS);
$qb = $repository->createQueryBuilder('e')->where('e.id IN (1, 2)');
$repo = $this->em->getRepository(self::SINGLE_IDENT_CLASS);
$entityType = new EntityType(
$this->emRegistry,
@ -799,19 +806,23 @@ class EntityTypeTest extends TypeTestCase
$formBuilder->add('property1', 'entity', array(
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'query_builder' => $qb,
'query_builder' => $repo->createQueryBuilder('e')->where('e.id IN (1, 2)'),
));
$formBuilder->add('property2', 'entity', array(
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'query_builder' => $qb,
'query_builder' => function (EntityRepository $repo) {
return $repo->createQueryBuilder('e')->where('e.id IN (1, 2)');
},
));
$formBuilder->add('property3', 'entity', array(
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'query_builder' => $qb,
'query_builder' => function (EntityRepository $repo) {
return $repo->createQueryBuilder('e')->where('e.id IN (1, 2)');
},
));
$form = $formBuilder->getForm();
@ -822,15 +833,59 @@ class EntityTypeTest extends TypeTestCase
'property3' => 2,
));
$reflectionClass = new \ReflectionObject($entityType);
$reflectionProperty = $reflectionClass->getProperty('loaderCache');
$reflectionProperty->setAccessible(true);
$choiceList1 = $form->get('property1')->getConfig()->getOption('choice_list');
$choiceList2 = $form->get('property2')->getConfig()->getOption('choice_list');
$choiceList3 = $form->get('property3')->getConfig()->getOption('choice_list');
$loaders = $reflectionProperty->getValue($entityType);
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\ChoiceListInterface', $choiceList1);
$this->assertSame($choiceList1, $choiceList2);
$this->assertSame($choiceList1, $choiceList3);
}
$reflectionProperty->setAccessible(false);
public function testCacheChoiceLists()
{
$entity1 = new SingleIntIdEntity(1, 'Foo');
$this->assertCount(1, $loaders);
$this->persist(array($entity1));
$field1 = $this->factory->createNamed('name', 'entity', null, array(
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'required' => false,
'choice_label' => 'name',
));
$field2 = $this->factory->createNamed('name', 'entity', null, array(
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'required' => false,
'choice_label' => 'name',
));
$this->assertInstanceOf('Symfony\Component\Form\ChoiceList\ChoiceListInterface', $field1->getConfig()->getOption('choice_list'));
$this->assertSame($field1->getConfig()->getOption('choice_list'), $field2->getConfig()->getOption('choice_list'));
}
/**
* @group legacy
*/
public function testLegacyPropertyOption()
{
$this->iniSet('error_reporting', -1 & ~E_USER_DEPRECATED);
$entity1 = new SingleIntIdEntity(1, 'Foo');
$entity2 = new SingleIntIdEntity(2, 'Bar');
$this->persist(array($entity1, $entity2));
$field = $this->factory->createNamed('name', 'entity', null, array(
'em' => 'default',
'class' => self::SINGLE_IDENT_CLASS,
'required' => false,
'property' => 'name',
));
$this->assertEquals(array(1 => new ChoiceView('Foo', '1', $entity1), 2 => new ChoiceView('Bar', '2', $entity2)), $field->createView()->vars['choices']);
}
protected function createRegistryMock($name, $em)

View File

@ -31,13 +31,13 @@ class LintCommand extends Command
/**
* {@inheritdoc}
*/
public function __construct($name = 'twig:lint')
public function __construct($name = 'lint:twig')
{
parent::__construct($name);
}
/**
* Sets the twig environment
* Sets the twig environment.
*
* @param \Twig_Environment $twig
*/
@ -57,6 +57,7 @@ class LintCommand extends Command
protected function configure()
{
$this
->setAliases(array('twig:lint'))
->setDescription('Lints a template and outputs encountered errors')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt')
->addArgument('filename', InputArgument::IS_ARRAY)
@ -83,6 +84,10 @@ EOF
protected function execute(InputInterface $input, OutputInterface $output)
{
if (false !== strpos($input->getFirstArgument(), ':l')) {
$output->writeln('<comment>The use of "twig:lint" command is deprecated since version 2.7 and will be removed in 3.0. Use the "lint:twig" instead.</comment>');
}
$twig = $this->getTwigEnvironment();
if (null === $twig) {
@ -95,7 +100,7 @@ EOF
if (0 === count($filenames)) {
if (0 !== ftell(STDIN)) {
throw new \RuntimeException("Please provide a filename or pipe template content to STDIN.");
throw new \RuntimeException('Please provide a filename or pipe template content to STDIN.');
}
$template = '';
@ -206,14 +211,14 @@ EOF
$line = $exception->getTemplateLine();
if ($file) {
$output->writeln(sprintf("<error>KO</error> in %s (line %s)", $file, $line));
$output->writeln(sprintf('<error>KO</error> in %s (line %s)', $file, $line));
} else {
$output->writeln(sprintf("<error>KO</error> (line %s)", $line));
$output->writeln(sprintf('<error>KO</error> (line %s)', $line));
}
foreach ($this->getContext($template, $line) as $no => $code) {
$output->writeln(sprintf(
"%s %-6s %s",
'%s %-6s %s',
$no == $line ? '<error>>></error>' : ' ',
$no,
$code

View File

@ -206,6 +206,9 @@ class CodeExtension extends \Twig_Extension
}, $text);
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'code';

View File

@ -13,7 +13,7 @@ namespace Symfony\Bridge\Twig\Extension;
use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser;
use Symfony\Bridge\Twig\Form\TwigRendererInterface;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
/**
* FormExtension extends Twig with form capabilities.

View File

@ -81,6 +81,9 @@ class HttpKernelExtension extends \Twig_Extension
return new ControllerReference($controller, $attributes, $query);
}
/**
* {@inheritdoc}
*/
public function getName()
{
return 'http_kernel';

View File

@ -89,9 +89,7 @@ class RoutingExtension extends \Twig_Extension
}
/**
* Returns the name of the extension.
*
* @return string The extension name
* {@inheritdoc}
*/
public function getName()
{

View File

@ -52,9 +52,7 @@ class SecurityExtension extends \Twig_Extension
}
/**
* Returns the name of the extension.
*
* @return string The extension name
* {@inheritdoc}
*/
public function getName()
{

View File

@ -99,9 +99,7 @@ class TranslationExtension extends \Twig_Extension
}
/**
* Returns the name of the extension.
*
* @return string The extension name
* {@inheritdoc}
*/
public function getName()
{

View File

@ -56,9 +56,7 @@ class YamlExtension extends \Twig_Extension
}
/**
* Returns the name of the extension.
*
* @return string The extension name
* {@inheritdoc}
*/
public function getName()
{

View File

@ -74,12 +74,13 @@
{%- block choice_widget_options -%}
{% for group_label, choice in options %}
{%- if choice is iterable -%}
<optgroup label="{{ group_label|trans({}, translation_domain) }}">
<optgroup label="{{ choice_translation_domain is sameas(false) ? group_label : group_label|trans({}, choice_translation_domain) }}">
{% set options = choice %}
{{- block('choice_widget_options') -}}
</optgroup>
{%- else -%}
<option value="{{ choice.value }}"{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice.label|trans({}, translation_domain) }}</option>
{% set attr = choice.attr %}
<option value="{{ choice.value }}" {{ block('attributes') }}{% if choice is selectedchoice(value) %} selected="selected"{% endif %}>{{ choice_translation_domain is sameas(false) ? choice.label : choice.label|trans({}, choice_translation_domain) }}</option>
{%- endif -%}
{% endfor %}
{%- endblock choice_widget_options -%}
@ -351,3 +352,16 @@
{%- endif -%}
{%- endfor -%}
{%- endblock button_attributes -%}
{% block attributes -%}
{%- for attrname, attrvalue in attr -%}
{{- " " -}}
{%- if attrname in ['placeholder', 'title'] -%}
{{- attrname }}="{{ attrvalue|trans({}, translation_domain) }}"
{%- elseif attrvalue is sameas(true) -%}
{{- attrname }}="{{ attrname }}"
{%- elseif attrvalue is not sameas(false) -%}
{{- attrname }}="{{ attrvalue }}"
{%- endif -%}
{%- endfor -%}
{%- endblock attributes -%}

View File

@ -80,7 +80,7 @@ class LintCommandTest extends \PHPUnit_Framework_TestCase
$application = new Application();
$application->add($command);
$command = $application->find('twig:lint');
$command = $application->find('lint:twig');
return new CommandTester($command);
}

View File

@ -17,8 +17,8 @@ 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;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\FormView;
use Symfony\Component\Form\Extension\Core\View\ChoiceView;
use Symfony\Component\Form\Tests\AbstractDivLayoutTest;
class FormExtensionDivLayoutTest extends AbstractDivLayoutTest
@ -125,7 +125,7 @@ class FormExtensionDivLayoutTest extends AbstractDivLayoutTest
*/
public function testIsChoiceSelected($expected, $choice, $value)
{
$choice = new ChoiceView($choice, $choice, $choice.' label');
$choice = new ChoiceView($choice.' label', $choice, $choice);
$this->assertSame($expected, $this->extension->isSelectedChoice($choice, $value));
}

View File

@ -5,6 +5,7 @@ CHANGELOG
-----
* Added possibility to extract translation messages from a file or files besides extracting from a directory
* Added `TranslationsCacheWarmer` to create catalogues at warmup
2.6.0
-----

View File

@ -0,0 +1,49 @@
<?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\CacheWarmer;
use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface;
use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
use Symfony\Component\Translation\TranslatorInterface;
/**
* Generates the catalogues for translations.
*
* @author Xavier Leune <xavier.leune@gmail.com>
*/
class TranslationsCacheWarmer implements CacheWarmerInterface
{
private $translator;
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
/**
* {@inheritdoc}
*/
public function warmUp($cacheDir)
{
if ($this->translator instanceof WarmableInterface) {
$this->translator->warmUp($cacheDir);
}
}
/**
* {@inheritdoc}
*/
public function isOptional()
{
return true;
}
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Bundle\FrameworkBundle\Command;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
@ -75,6 +76,12 @@ EOF
$output->writeln('<error>This command needs the pcntl extension to run.</error>');
$output->writeln('You can either install it or use the <info>server:run</info> command instead to run the built-in web server.');
if ($this->getHelper('question')->ask($input, $output, new ConfirmationQuestion('Do you want to start <info>server:run</info> immediately? [Yn] ', true))) {
$command = $this->getApplication()->find('server:run');
return $command->run($input, $output);
}
return 1;
}

View File

@ -29,7 +29,8 @@ class YamlLintCommand extends Command
protected function configure()
{
$this
->setName('yaml:lint')
->setName('lint:yaml')
->setAliases(array('yaml:lint'))
->setDescription('Lints a file and outputs encountered errors')
->addArgument('filename', null, 'A file or a directory or STDIN')
->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt')
@ -61,6 +62,10 @@ EOF
protected function execute(InputInterface $input, OutputInterface $output)
{
if (false !== strpos($input->getFirstArgument(), ':l')) {
$output->writeln('<comment>The use of "yaml:lint" command is deprecated since version 2.7 and will be removed in 3.0. Use the "lint:yaml" instead.</comment>');
}
$filename = $input->getArgument('filename');
if (!$filename) {

View File

@ -318,7 +318,7 @@ abstract class Controller extends ContainerAware
}
/**
* Gets a service by id.
* Gets a container service by its id.
*
* @param string $id The service id
*
@ -329,10 +329,22 @@ abstract class Controller extends ContainerAware
if ('request' === $id) {
trigger_error('The "request" service is deprecated and will be removed in 3.0. Add a typehint for Symfony\\Component\\HttpFoundation\\Request to your controller parameters to retrieve the request instead.', E_USER_DEPRECATED);
}
return $this->container->get($id);
}
/**
* Gets a container configuration parameter by its name.
*
* @param string $name The parameter name
*
* @return mixed
*/
protected function getParameter($name)
{
return $this->container->getParameter($name);
}
/**
* Checks the validity of a CSRF token
*

View File

@ -13,6 +13,7 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;
/**
* @author Abdellatif Ait boudad <a.aitboudad@gmail.com>
@ -38,6 +39,7 @@ class LoggingTranslatorPass implements CompilerPassInterface
$refClass = new \ReflectionClass($class);
if ($refClass->implementsInterface('Symfony\Component\Translation\TranslatorInterface') && $refClass->implementsInterface('Symfony\Component\Translation\TranslatorBagInterface')) {
$container->getDefinition('translator.logging')->setDecoratedService('translator');
$container->getDefinition('translation.warmer')->replaceArgument(0, new Reference('translator.logging.inner'));
}
}
}

View File

@ -89,10 +89,7 @@ class Configuration implements ConfigurationInterface
->booleanNode('test')->end()
->scalarNode('default_locale')->defaultValue('en')->end()
->arrayNode('trusted_hosts')
->beforeNormalization()
->ifTrue(function ($v) { return is_string($v); })
->then(function ($v) { return array($v); })
->end()
->beforeNormalization()->ifString()->then(function ($v) { return array($v); })->end()
->prototype('scalar')->end()
->end()
->end()
@ -338,7 +335,7 @@ class Configuration implements ConfigurationInterface
->addDefaultChildrenIfNoneSet()
->prototype('scalar')->defaultValue('FrameworkBundle:Form')->end()
->validate()
->ifTrue(function ($v) {return !in_array('FrameworkBundle:Form', $v); })
->ifNotInArray(array('FrameworkBundle:Form'))
->then(function ($v) {
return array_merge(array('FrameworkBundle:Form'), $v);
})

View File

@ -705,12 +705,22 @@ class FrameworkExtension extends Extension
->in($dirs)
;
$locales = array();
foreach ($finder as $file) {
list($domain, $locale, $format) = explode('.', $file->getBasename(), 3);
$files[] = (string) $file;
if (!isset($files[$locale])) {
$files[$locale] = array();
}
$files[$locale][] = (string) $file;
}
$translator->replaceArgument(4, $files);
$options = array_merge(
$translator->getArgument(3),
array('resource_files' => $files)
);
$translator->replaceArgument(3, $options);
}
}

View File

@ -14,7 +14,7 @@
<tag name="monolog.logger" channel="php" />
<argument /><!-- Exception handler -->
<argument type="service" id="logger" on-invalid="null" />
<argument /><!-- Log levels map for enabled error levels -->
<argument>null</argument><!-- Log levels map for enabled error levels -->
<argument>null</argument>
<argument>true</argument>
<argument>null</argument><!-- %templating.helper.code.file_link_format% -->

View File

@ -121,5 +121,10 @@
<service id="translation.extractor" class="Symfony\Component\Translation\Extractor\ChainExtractor"/>
<service id="translation.writer" class="Symfony\Component\Translation\Writer\TranslationWriter"/>
<service id="translation.warmer" class="Symfony\Bundle\FrameworkBundle\CacheWarmer\TranslationsCacheWarmer" public="false">
<argument type="service" id="translator" />
<tag name="kernel.cache_warmer" />
</service>
</services>
</container>

View File

@ -1,11 +1,13 @@
<?php $translatorHelper = $view['translator']; // outside of the loop for performance reasons! ?>
<?php use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
$translatorHelper = $view['translator']; // outside of the loop for performance reasons! ?>
<?php $formHelper = $view['form']; ?>
<?php foreach ($choices as $index => $choice): ?>
<?php if (is_array($choice)): ?>
<optgroup label="<?php echo $view->escape($translatorHelper->trans($index, array(), $translation_domain)) ?>">
<?php foreach ($choices as $group_label => $choice): ?>
<?php if (is_array($choice) || $choice instanceof ChoiceGroupView): ?>
<optgroup label="<?php echo $view->escape(false !== $choice_translation_domain ? $translatorHelper->trans($group_label, array(), $choice_translation_domain) : $group_label) ?>">
<?php echo $formHelper->block($form, 'choice_widget_options', array('choices' => $choice)) ?>
</optgroup>
<?php else: ?>
<option value="<?php echo $view->escape($choice->value) ?>"<?php if ($is_selected($choice->value, $value)): ?> selected="selected"<?php endif?>><?php echo $view->escape($translatorHelper->trans($choice->label, array(), $translation_domain)) ?></option>
<option value="<?php echo $view->escape($choice->value) ?>" <?php echo $view['form']->block($form, 'attributes', array('attr' => $choice->attr)) ?><?php if ($is_selected($choice->value, $value)): ?> selected="selected"<?php endif?>><?php echo $view->escape(false !== $choice_translation_domain ? $translatorHelper->trans($choice->label, array(), $choice_translation_domain) : $choice->label) ?></option>
<?php endif ?>
<?php endforeach ?>

View File

@ -58,9 +58,7 @@ class ActionsHelper extends Helper
}
/**
* Returns the canonical name of this helper.
*
* @return string The canonical name
* {@inheritdoc}
*/
public function getName()
{

View File

@ -199,9 +199,7 @@ class CodeHelper extends Helper
}
/**
* Returns the canonical name of this helper.
*
* @return string The canonical name
* {@inheritdoc}
*/
public function getName()
{

View File

@ -83,9 +83,7 @@ class RequestHelper extends Helper
}
/**
* Returns the canonical name of this helper.
*
* @return string The canonical name
* {@inheritdoc}
*/
public function getName()
{

View File

@ -50,9 +50,7 @@ class RouterHelper extends Helper
}
/**
* Returns the canonical name of this helper.
*
* @return string The canonical name
* {@inheritdoc}
*/
public function getName()
{

View File

@ -86,9 +86,7 @@ class SessionHelper extends Helper
}
/**
* Returns the canonical name of this helper.
*
* @return string The canonical name
* {@inheritdoc}
*/
public function getName()
{

View File

@ -50,9 +50,7 @@ class TranslatorHelper extends Helper
}
/**
* Returns the canonical name of this helper.
*
* @return string The canonical name
* {@inheritdoc}
*/
public function getName()
{

View File

@ -35,7 +35,7 @@ class LoggingTranslatorPassTest extends \PHPUnit_Framework_TestCase
->method('getAlias')
->will($this->returnValue('translation.default'));
$container->expects($this->exactly(2))
$container->expects($this->exactly(3))
->method('getDefinition')
->will($this->returnValue($definition));

View File

@ -234,9 +234,9 @@ abstract class FrameworkExtensionTest extends TestCase
$container = $this->createContainerFromFile('full');
$this->assertTrue($container->hasDefinition('translator.default'), '->registerTranslatorConfiguration() loads translation.xml');
$this->assertEquals('translator.default', (string) $container->getAlias('translator'), '->registerTranslatorConfiguration() redefines translator service from identity to real translator');
$resources = $container->getDefinition('translator.default')->getArgument(4);
$options = $container->getDefinition('translator.default')->getArgument(3);
$files = array_map(function ($resource) { return realpath($resource); }, $resources);
$files = array_map(function ($resource) { return realpath($resource); }, $options['resource_files']['en']);
$ref = new \ReflectionClass('Symfony\Component\Validator\Validation');
$this->assertContains(
strtr(dirname($ref->getFileName()).'/Resources/translations/validators.en.xlf', '/', DIRECTORY_SEPARATOR),

View File

@ -95,7 +95,7 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase
public function testTransWithCachingWithInvalidLocale()
{
$loader = $this->getMock('Symfony\Component\Translation\Loader\LoaderInterface');
$translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), array(), 'loader', '\Symfony\Bundle\FrameworkBundle\Tests\Translation\TranslatorWithInvalidLocale');
$translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), 'loader', '\Symfony\Bundle\FrameworkBundle\Tests\Translation\TranslatorWithInvalidLocale');
$translator->setLocale('invalid locale');
$this->setExpectedException('\InvalidArgumentException');
@ -106,23 +106,25 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase
{
$loader = new \Symfony\Component\Translation\Loader\YamlFileLoader();
$resourceFiles = array(
__DIR__.'/../Fixtures/Resources/translations/messages.fr.yml',
'fr' => array(
__DIR__.'/../Fixtures/Resources/translations/messages.fr.yml',
),
);
// prime the cache
$translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), $resourceFiles, 'yml');
$translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'resource_files' => $resourceFiles), 'yml');
$translator->setLocale('fr');
$this->assertEquals('répertoire', $translator->trans('folder'));
// do it another time as the cache is primed now
$translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), array(), 'yml');
$translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir), 'yml');
$translator->setLocale('fr');
$this->assertEquals('répertoire', $translator->trans('folder'));
// refresh cache when resources is changed in debug mode.
$translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'debug' => true), array(), 'yml');
$translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'debug' => true), 'yml');
$translator->setLocale('fr');
$this->assertEquals('folder', $translator->trans('folder'));
@ -132,10 +134,12 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase
{
$loader = new \Symfony\Component\Translation\Loader\YamlFileLoader();
$resourceFiles = array(
__DIR__.'/../Fixtures/Resources/translations/messages.fr.yml',
'fr' => array(
__DIR__.'/../Fixtures/Resources/translations/messages.fr.yml',
),
);
$translator = $this->getTranslator($loader, array(), $resourceFiles, 'yml');
$translator = $this->getTranslator($loader, array('resource_files' => $resourceFiles), 'yml');
$translator->setLocale('fr');
$this->assertEquals('répertoire', $translator->trans('folder'));
@ -221,14 +225,13 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase
return $container;
}
public function getTranslator($loader, $options = array(), $resources = array(), $loaderFomat = 'loader', $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator')
public function getTranslator($loader, $options = array(), $loaderFomat = 'loader', $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator')
{
$translator = new $translatorClass(
$this->getContainer($loader),
new MessageSelector(),
array($loaderFomat => array($loaderFomat)),
$options,
$resources
$options
);
if ('loader' === $loaderFomat) {
@ -243,6 +246,22 @@ class TranslatorTest extends \PHPUnit_Framework_TestCase
return $translator;
}
public function testWarmup()
{
$loader = new \Symfony\Component\Translation\Loader\YamlFileLoader();
$resourceFiles = array(
'fr' => array(
__DIR__.'/../Fixtures/Resources/translations/messages.fr.yml',
),
);
// prime the cache
$translator = $this->getTranslator($loader, array('cache_dir' => $this->tmpDir, 'resource_files' => $resourceFiles), 'yml');
$this->assertFalse(file_exists($this->tmpDir.'/catalogue.fr.php'));
$translator->warmup($this->tmpDir);
$this->assertTrue(file_exists($this->tmpDir.'/catalogue.fr.php'));
}
}
class TranslatorWithInvalidLocale extends Translator

View File

@ -11,6 +11,7 @@
namespace Symfony\Bundle\FrameworkBundle\Translation;
use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface;
use Symfony\Component\Translation\Translator as BaseTranslator;
use Symfony\Component\Translation\MessageSelector;
use Symfony\Component\DependencyInjection\ContainerInterface;
@ -20,17 +21,22 @@ use Symfony\Component\DependencyInjection\ContainerInterface;
*
* @author Fabien Potencier <fabien@symfony.com>
*/
class Translator extends BaseTranslator
class Translator extends BaseTranslator implements WarmableInterface
{
protected $container;
protected $loaderIds;
protected $resourceFiles;
protected $options = array(
'cache_dir' => null,
'debug' => false,
'resource_files' => array(),
);
/**
* @var array
*/
private $resourceLocales;
/**
* Constructor.
*
@ -38,20 +44,19 @@ class Translator extends BaseTranslator
*
* * cache_dir: The cache directory (or null to disable caching)
* * debug: Whether to enable debugging or not (false by default)
* * resource_files: List of translation resources available grouped by locale.
*
* @param ContainerInterface $container A ContainerInterface instance
* @param MessageSelector $selector The message selector for pluralization
* @param array $loaderIds An array of loader Ids
* @param array $options An array of options
* @param array $resourceFiles An array of resource directories
* @param ContainerInterface $container A ContainerInterface instance
* @param MessageSelector $selector The message selector for pluralization
* @param array $loaderIds An array of loader Ids
* @param array $options An array of options
*
* @throws \InvalidArgumentException
*/
public function __construct(ContainerInterface $container, MessageSelector $selector, $loaderIds = array(), array $options = array(), $resourceFiles = array())
public function __construct(ContainerInterface $container, MessageSelector $selector, $loaderIds = array(), array $options = array())
{
$this->container = $container;
$this->loaderIds = $loaderIds;
$this->resourceFiles = $resourceFiles;
// check option names
if ($diff = array_diff(array_keys($options), array_keys($this->options))) {
@ -59,6 +64,7 @@ class Translator extends BaseTranslator
}
$this->options = array_merge($this->options, $options);
$this->resourceLocales = array_keys($this->options['resource_files']);
if (null !== $this->options['cache_dir'] && $this->options['debug']) {
$this->loadResources();
}
@ -66,6 +72,16 @@ class Translator extends BaseTranslator
parent::__construct(null, $selector, $this->options['cache_dir'], $this->options['debug']);
}
/**
* {@inheritdoc}
*/
public function warmUp($cacheDir)
{
foreach ($this->resourceLocales as $locale) {
$this->loadCatalogue($locale);
}
}
/**
* {@inheritdoc}
*/
@ -87,11 +103,13 @@ class Translator extends BaseTranslator
private function loadResources()
{
foreach ($this->resourceFiles as $key => $file) {
// filename is domain.locale.format
list($domain, $locale, $format) = explode('.', basename($file), 3);
$this->addResource($format, $file, $locale, $domain);
unset($this->resourceFiles[$key]);
foreach ($this->options['resource_files'] as $locale => $files) {
foreach ($files as $key => $file) {
// filename is domain.locale.format
list($domain, $locale, $format) = explode('.', basename($file), 3);
$this->addResource($format, $file, $locale, $domain);
unset($this->options['resource_files'][$locale][$key]);
}
}
}
}

View File

@ -53,7 +53,7 @@
"symfony/finder": "For using the translation loader and cache warmer",
"symfony/form": "For using forms",
"symfony/validator": "For using validation",
"symfony/yaml": "For using the debug:config and yaml:lint commands",
"symfony/yaml": "For using the debug:config and lint:yaml commands",
"doctrine/cache": "For using alternative cache drivers"
},
"autoload": {

View File

@ -25,9 +25,6 @@ use Symfony\Component\Templating\Helper\Helper;
class LogoutUrlHelper extends Helper
{
private $generator;
private $listeners = array();
private $router;
private $tokenStorage;
/**
* Constructor.
@ -79,9 +76,7 @@ class LogoutUrlHelper extends Helper
}
/**
* Returns the canonical name of this helper.
*
* @return string The canonical name
* {@inheritdoc}
*/
public function getName()
{

View File

@ -43,9 +43,7 @@ class SecurityHelper extends Helper
}
/**
* Returns the canonical name of this helper.
*
* @return string The canonical name
* {@inheritdoc}
*/
public function getName()
{

View File

@ -4,6 +4,7 @@ CHANGELOG
2.7.0
-----
* made it possible to configure the default formats for both the `date` and the `number_format` filter
* added support for the new Asset component (from Twig bridge)
* deprecated the assets extension (use the one from the Twig bridge instead)

View File

@ -59,7 +59,7 @@ class LintCommand extends BaseLintCommand implements ContainerAwareInterface
Or all template files in a bundle:
<info>php %command.full_name% @AcmeDemoBundle</info>
EOF
)
;

View File

@ -41,6 +41,7 @@ class Configuration implements ConfigurationInterface
$this->addFormThemesSection($rootNode);
$this->addGlobalsSection($rootNode);
$this->addTwigOptions($rootNode);
$this->addTwigFormatOptions($rootNode);
return $treeBuilder;
}
@ -160,4 +161,33 @@ class Configuration implements ConfigurationInterface
->end()
;
}
private function addTwigFormatOptions(ArrayNodeDefinition $rootNode)
{
$rootNode
->children()
->arrayNode('date')
->info('The default format options used by the date filter')
->addDefaultsIfNotSet()
->children()
->scalarNode('format')->defaultValue('F j, Y H:i')->end()
->scalarNode('interval_format')->defaultValue('%d days')->end()
->scalarNode('timezone')
->info('The timezone used when formatting dates, when set to null, the timezone returned by date_default_timezone_get() is used')
->defaultNull()
->end()
->end()
->end()
->arrayNode('number_format')
->info('The default format options for the number_format filter')
->addDefaultsIfNotSet()
->children()
->integerNode('decimals')->defaultValue(0)->end()
->scalarNode('decimal_point')->defaultValue('.')->end()
->scalarNode('thousands_separator')->defaultValue(',')->end()
->end()
->end()
->end()
;
}
}

View File

@ -0,0 +1,48 @@
<?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\TwigBundle\DependencyInjection\Configurator;
/**
* Twig environment configurator.
*
* @author Christian Flothmann <christian.flothmann@xabbuh.de>
*/
class EnvironmentConfigurator
{
private $dateFormat;
private $intervalFormat;
private $timezone;
private $decimals;
private $decimalPoint;
private $thousandsSeparator;
public function __construct($dateFormat, $intervalFormat, $timezone, $decimals, $decimalPoint, $thousandsSeparator)
{
$this->dateFormat = $dateFormat;
$this->intervalFormat = $intervalFormat;
$this->timezone = $timezone;
$this->decimals = $decimals;
$this->decimalPoint = $decimalPoint;
$this->thousandsSeparator = $thousandsSeparator;
}
public function configure(\Twig_Environment $environment)
{
$environment->getExtension('core')->setDateFormat($this->dateFormat, $this->intervalFormat);
if (null !== $this->timezone) {
$environment->getExtension('core')->setTimezone($this->timezone);
}
$environment->getExtension('core')->setNumberFormat($this->decimals, $this->decimalPoint, $this->thousandsSeparator);
}
}

View File

@ -57,6 +57,14 @@ class TwigExtension extends Extension
$container->setParameter('twig.form.resources', $config['form_themes']);
$envConfiguratorDefinition = $container->getDefinition('twig.configurator.environment');
$envConfiguratorDefinition->replaceArgument(0, $config['date']['format']);
$envConfiguratorDefinition->replaceArgument(1, $config['date']['interval_format']);
$envConfiguratorDefinition->replaceArgument(2, $config['date']['timezone']);
$envConfiguratorDefinition->replaceArgument(3, $config['number_format']['decimals']);
$envConfiguratorDefinition->replaceArgument(4, $config['number_format']['decimal_point']);
$envConfiguratorDefinition->replaceArgument(5, $config['number_format']['thousands_separator']);
$twigFilesystemLoaderDefinition = $container->getDefinition('twig.loader.filesystem');
// register user-configured paths

View File

@ -12,6 +12,7 @@
<argument>app</argument>
<argument type="service" id="twig.app_variable" />
</call>
<configurator service="twig.configurator.environment" method="configure" />
</service>
<service id="twig.app_variable" class="Symfony\Bridge\Twig\AppVariable" public="false">
@ -135,5 +136,14 @@
<argument type="service" id="http_kernel" />
<argument>%twig.exception_listener.controller%</argument>
</service>
<service id="twig.configurator.environment" class="Symfony\Bundle\TwigBundle\DependencyInjection\Configurator\EnvironmentConfigurator" public="false">
<argument /> <!-- date format, set in TwigExtension -->
<argument /> <!-- interval format, set in TwigExtension -->
<argument /> <!-- timezone, set in TwigExtension -->
<argument /> <!-- decimals, set in TwigExtension -->
<argument /> <!-- decimal point, set in TwigExtension -->
<argument /> <!-- thousands separator, set in TwigExtension -->
</service>
</services>
</container>

View File

@ -9,7 +9,18 @@
{% if collector.applicationname %}
{{ collector.applicationname }} {{ collector.applicationversion }}
{% else %}
{{ collector.symfonyversion }}
{% if 'unknown' == collector.symfonyState -%}
<span class="sf-toolbar-status sf-toolbar-info-piece-additional" title="Unable to retrieve information about the Symfony version.">
{%- elseif 'eol' == collector.symfonyState -%}
<span class="sf-toolbar-status sf-toolbar-status-red" title="This Symfony version will no longer receive security fixes.">
{%- elseif 'eom' == collector.symfonyState -%}
<span class="sf-toolbar-status sf-toolbar-status-yellow" title="This Symfony version will only receive security fixes.">
{%- elseif 'dev' == collector.symfonyState -%}
<span class="sf-toolbar-status sf-toolbar-status-yellow" title="This Symfony version is still in the development phase.">
{%- else -%}
<span class="sf-toolbar-status sf-toolbar-status-green">
{%- endif -%}
{{ collector.symfonyversion }}</span>
{% endif %}
</span>
</a>

View File

@ -88,10 +88,13 @@
{% if collector.logs %}
<ul class="alt">
{% for log in collector.logs if priority >= 0 and log.priority >= priority or priority < 0 and log.context.type|default(0) == priority %}
<li class="{{ cycle(['odd', 'even'], loop.index) }}{% if log.priority >= 400 %} error{% elseif log.priority >= 300 %} warning{% endif %}{% if log.context.scream is defined %} scream{% endif %}">
{{ logger.display_message(loop.index, log) }}
</li>
{% for log in collector.logs %}
{% set is_deprecation = log.context.level is defined and log.context.type is defined and (constant('E_DEPRECATED') == log.context.type or constant('E_USER_DEPRECATED') == log.context.type) %}
{% if priority == '-100' ? is_deprecation : log.priority >= priority %}
<li class="{{ cycle(['odd', 'even'], loop.index) }}{% if log.context.scream is defined %} scream{% elseif log.priority >= 400 %} error{% elseif log.priority >= 300 %} warning{% endif %}">
{{ logger.display_message(loop.index, log, is_deprecation) }}
</li>
{% endif %}
{% else %}
<li><em>No logs available for this priority.</em></li>
{% endfor %}
@ -104,15 +107,18 @@
{% endblock %}
{% macro display_message(log_index, log) %}
{% if log.context.level is defined and log.context.type is defined and (constant('E_DEPRECATED') == log.context.type or constant('E_USER_DEPRECATED') == log.context.type) %}
DEPRECATION - {{ log.message }}
{% set id = 'sf-call-stack-' ~ log_index %}
<a href="#" onclick="Sfjs.toggle('{{ id }}', document.getElementById('{{ id }}-on'), document.getElementById('{{ id }}-off')); return false;">
<img class="toggle" id="{{ id }}-off" alt="-" src="data:image/gif;base64,R0lGODlhEgASAMQSANft94TG57Hb8GS44ez1+mC24IvK6ePx+Wa44dXs92+942e54o3L6W2844/M6dnu+P/+/l614P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABIALAAAAAASABIAQAVCoCQBTBOd6Kk4gJhGBCTPxysJb44K0qD/ER/wlxjmisZkMqBEBW5NHrMZmVKvv9hMVsO+hE0EoNAstEYGxG9heIhCADs=" style="display:none">
<img class="toggle" id="{{ id }}-on" alt="+" src="data:image/gif;base64,R0lGODlhEgASAMQTANft99/v+Ga44bHb8ITG52S44dXs9+z1+uPx+YvK6WC24G+944/M6W28443L6dnu+Ge54v/+/l614P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABMALAAAAAASABIAQAVS4DQBTiOd6LkwgJgeUSzHSDoNaZ4PU6FLgYBA5/vFID/DbylRGiNIZu74I0h1hNsVxbNuUV4d9SsZM2EzWe1qThVzwWFOAFCQFa1RQq6DJB4iIQA7" style="display:inline">
</a>
{% for index, call in log.context.stack if index > 1 %}
{% macro display_message(log_index, log, is_deprecation) %}
{% if is_deprecation %}
{% set stack = log.context.stack|default([]) %}
{% if stack %}
<a href="#" onclick="Sfjs.toggle('{{ id }}', document.getElementById('{{ id }}-on'), document.getElementById('{{ id }}-off')); return false;">
<img class="toggle" id="{{ id }}-off" alt="-" src="data:image/gif;base64,R0lGODlhEgASAMQSANft94TG57Hb8GS44ez1+mC24IvK6ePx+Wa44dXs92+942e54o3L6W2844/M6dnu+P/+/l614P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABIALAAAAAASABIAQAVCoCQBTBOd6Kk4gJhGBCTPxysJb44K0qD/ER/wlxjmisZkMqBEBW5NHrMZmVKvv9hMVsO+hE0EoNAstEYGxG9heIhCADs=" style="display:none">
<img class="toggle" id="{{ id }}-on" alt="+" src="data:image/gif;base64,R0lGODlhEgASAMQTANft99/v+Ga44bHb8ITG52S44dXs9+z1+uPx+YvK6WC24G+944/M6W28443L6dnu+Ge54v/+/l614P///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAABMALAAAAAASABIAQAVS4DQBTiOd6LkwgJgeUSzHSDoNaZ4PU6FLgYBA5/vFID/DbylRGiNIZu74I0h1hNsVxbNuUV4d9SsZM2EzWe1qThVzwWFOAFCQFa1RQq6DJB4iIQA7" style="display:inline">
</a>
{% endif %}
{% for index, call in stack if index > 1 %}
{% if index == 2 %}
<ul class="sf-call-stack" id="{{ id }}" style="display: none">
{% endif %}
@ -128,7 +134,7 @@
<li>Called from {{ call.file is defined and call.line is defined ? call.file|format_file(call.line, from) : from|raw }}</li>
{% if index == log.context.stack|length - 1 %}
{% if index == stack|length - 1 %}
</ul>
{% endif %}
{% endfor %}

View File

@ -57,12 +57,23 @@
{% endblock %}
{% block panelContent %}
<h2>Called Translations</h2>
<ul>
<li><strong>Defined messages: {{ collector.countdefines }}</strong></li>
<li><strong>Fallback messages: {{ collector.countFallbacks }}</strong></li>
<li><strong>Missing messages: {{ collector.countMissings }}</strong></li>
</ul>
<h2>Translation Stats</h2>
<table>
<tbody>
<tr>
<th>Defined messages</th>
<td><pre>{{ collector.countdefines }}</pre></td>
</tr>
<tr>
<th scope="col" style="width: 30%">Fallback messages</th>
<td scope="col" style="width: 60%"><pre>{{ collector.countFallbacks }}</pre></td>
</tr>
<tr>
<th>Missing messages</th>
<td><pre>{{ collector.countMissings }}</pre></td>
</tr>
</tbody>
</table>
<table>
<tr>
@ -77,7 +88,10 @@
<td><code>{{ translator.state(message) }}</code></td>
<td><code>{{ message.locale }}</code></td>
<td><code>{{ message.domain }}</code></td>
<td><code>{{ message.id }}</code></td>
<td>
<code>{{ message.id }}</code>
{% if message.count > 1 %}<br><small style="color: gray;">(used {{ message.count }} times)</small>{% endif %}
</td>
<td><code>{{ message.translation }}</code></td>
</tr>
{% endfor %}

View File

@ -199,7 +199,7 @@
}
{% if excluded_ajax_paths is defined %}
if (window.XMLHttpRequest && XMLHttpRequest.addEventListener) {
if (window.XMLHttpRequest && XMLHttpRequest.prototype.addEventListener) {
var proxied = XMLHttpRequest.prototype.open;
XMLHttpRequest.prototype.open = function(method, url, async, user, pass) {

View File

@ -781,7 +781,7 @@ class Application
$input->setInteractive(false);
} elseif (function_exists('posix_isatty') && $this->getHelperSet()->has('question')) {
$inputStream = $this->getHelperSet()->get('question')->getInputStream();
if (!@posix_isatty($inputStream)) {
if (!@posix_isatty($inputStream) && false === getenv('SHELL_INTERACTIVE')) {
$input->setInteractive(false);
}
}

View File

@ -97,7 +97,7 @@ class SymfonyStyle extends OutputStyle
*/
public function title($message)
{
$this->writeln(sprintf("\n<fg=blue>%s</fg=blue>\n<fg=blue>%s</fg=blue>\n", $message, str_repeat('=', strlen($message))));
$this->writeln(sprintf("\n<comment>%s</>\n<comment>%s</>\n", $message, str_repeat('=', strlen($message))));
}
/**
@ -105,7 +105,7 @@ class SymfonyStyle extends OutputStyle
*/
public function section($message)
{
$this->writeln(sprintf("<fg=blue>%s</fg=blue>\n<fg=blue>%s</fg=blue>\n", $message, str_repeat('-', strlen($message))));
$this->writeln(sprintf("<comment>%s</>\n<comment>%s</>\n", $message, str_repeat('-', strlen($message))));
}
/**
@ -119,7 +119,7 @@ class SymfonyStyle extends OutputStyle
$elements
);
$this->writeln(implode("\n\n", $elements)."\n");
$this->writeln(implode("\n", $elements)."\n");
}
/**
@ -183,6 +183,8 @@ class SymfonyStyle extends OutputStyle
*/
public function table(array $headers, array $rows)
{
$headers = array_map(function ($value) { return sprintf('<info>%s</>', $value); }, $headers);
$table = new Table($this);
$table->setHeaders($headers);
$table->setRows($rows);

View File

@ -61,7 +61,7 @@ class CommandTester
&& (null !== $application = $this->command->getApplication())
&& $application->getDefinition()->hasArgument('command')
) {
$input['command'] = $this->command->getName();
$input = array_merge(array('command' => $this->command->getName()), $input);
}
$this->input = new ArrayInput($input);

View File

@ -6,6 +6,14 @@ CHANGELOG
* removed classes, methods and interfaces deprecated in 2.x
2.7.0
-----
* added deprecations checking for parent interfaces/classes to DebugClassLoader
* added ZTS support to symfony_debug extension
* added symfony_debug_backtrace() to symfony_debug extension
to track the backtrace of fatal errors
2.6.0
-----

View File

@ -66,9 +66,9 @@ class ErrorHandler
private $loggers = array(
E_DEPRECATED => array(null, LogLevel::INFO),
E_USER_DEPRECATED => array(null, LogLevel::INFO),
E_NOTICE => array(null, LogLevel::NOTICE),
E_USER_NOTICE => array(null, LogLevel::NOTICE),
E_STRICT => array(null, LogLevel::NOTICE),
E_NOTICE => array(null, LogLevel::WARNING),
E_USER_NOTICE => array(null, LogLevel::WARNING),
E_STRICT => array(null, LogLevel::WARNING),
E_WARNING => array(null, LogLevel::WARNING),
E_USER_WARNING => array(null, LogLevel::WARNING),
E_COMPILE_WARNING => array(null, LogLevel::WARNING),
@ -223,7 +223,7 @@ class ErrorHandler
}
/**
* Sets the error levels that are to be thrown.
* Sets the PHP error levels that throw an exception when a PHP error occurs.
*
* @param int $levels A bit field of E_* constants for thrown errors
* @param bool $replace Replace or amend the previous value
@ -243,7 +243,7 @@ class ErrorHandler
}
/**
* Sets the error levels that are logged or thrown with their local scope.
* Sets the PHP error levels for which local variables are preserved.
*
* @param int $levels A bit field of E_* constants for scoped errors
* @param bool $replace Replace or amend the previous value
@ -262,7 +262,7 @@ class ErrorHandler
}
/**
* Sets the error levels that are logged with their stack trace.
* Sets the PHP error levels for which the stack trace is preserved.
*
* @param int $levels A bit field of E_* constants for traced errors
* @param bool $replace Replace or amend the previous value
@ -336,55 +336,57 @@ class ErrorHandler
$throw = $this->thrownErrors & $type & $level;
$type &= $level | $this->screamedErrors;
if ($type && ($log || $throw)) {
if ($throw) {
if (($this->scopedErrors & $type) && class_exists('Symfony\Component\Debug\Exception\ContextErrorException')) {
// Checking for class existence is a work around for https://bugs.php.net/42098
$throw = new ContextErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line, $context);
} else {
$throw = new \ErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line);
}
if (!$type || (!$log && !$throw)) {
return $type && $log;
}
throw $throw;
}
// For duplicated errors, log the trace only once
$e = md5("{$type}/{$line}/{$file}\x00{$message}", true);
$trace = true;
if (!($this->tracedErrors & $type) || isset($this->loggedTraces[$e])) {
$trace = false;
if ($throw) {
if (($this->scopedErrors & $type) && class_exists(ContextErrorException::class)) {
// Checking for class existence is a work around for https://bugs.php.net/42098
$throw = new ContextErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line, $context);
} else {
$this->loggedTraces[$e] = 1;
$throw = new \ErrorException($this->levels[$type].': '.$message, 0, $type, $file, $line);
}
$e = compact('type', 'file', 'line', 'level');
throw $throw;
}
if ($type & $level) {
if ($this->scopedErrors & $type) {
$e['context'] = $context;
if ($trace) {
$e['stack'] = debug_backtrace(true); // Provide object
}
} elseif ($trace) {
$e['stack'] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
// For duplicated errors, log the trace only once
$e = md5("{$type}/{$line}/{$file}\x00{$message}", true);
$trace = true;
if (!($this->tracedErrors & $type) || isset($this->loggedTraces[$e])) {
$trace = false;
} else {
$this->loggedTraces[$e] = 1;
}
$e = compact('type', 'file', 'line', 'level');
if ($type & $level) {
if ($this->scopedErrors & $type) {
$e['scope_vars'] = $context;
if ($trace) {
$e['stack'] = debug_backtrace(DEBUG_BACKTRACE_PROVIDE_OBJECT);
}
} elseif ($trace) {
$e['stack'] = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
}
}
if ($this->isRecursive) {
$log = 0;
} elseif (self::$stackedErrorLevels) {
self::$stackedErrors[] = array($this->loggers[$type], $message, $e);
} else {
try {
$this->isRecursive = true;
$this->loggers[$type][0]->log($this->loggers[$type][1], $message, $e);
$this->isRecursive = false;
} catch (\Exception $e) {
$this->isRecursive = false;
if ($this->isRecursive) {
$log = 0;
} elseif (self::$stackedErrorLevels) {
self::$stackedErrors[] = array($this->loggers[$type], $message, $e);
} else {
try {
$this->isRecursive = true;
$this->loggers[$type][0]->log($this->loggers[$type][1], $message, $e);
$this->isRecursive = false;
} catch (\Exception $e) {
$this->isRecursive = false;
throw $e;
}
throw $e;
}
}
@ -453,40 +455,44 @@ class ErrorHandler
public static function handleFatalError(array $error = null)
{
self::$reservedMemory = '';
$handler = set_error_handler('var_dump', 0);
$handler = is_array($handler) ? $handler[0] : null;
restore_error_handler();
if ($handler instanceof self) {
if (null === $error) {
$error = error_get_last();
}
try {
while (self::$stackedErrorLevels) {
static::unstackErrors();
}
} catch (\Exception $exception) {
// Handled below
}
if (!$handler instanceof self) {
return;
}
if ($error && ($error['type'] & (E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR))) {
// Let's not throw anymore but keep logging
$handler->throwAt(0, true);
if (null === $error) {
$error = error_get_last();
}
if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) {
$exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false);
} else {
$exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true);
}
} elseif (!isset($exception)) {
return;
try {
while (self::$stackedErrorLevels) {
static::unstackErrors();
}
} catch (\Exception $exception) {
// Handled below
}
try {
$handler->handleException($exception, $error);
} catch (FatalErrorException $e) {
// Ignore this re-throw
if ($error && ($error['type'] & (E_PARSE | E_ERROR | E_CORE_ERROR | E_COMPILE_ERROR))) {
// Let's not throw anymore but keep logging
$handler->throwAt(0, true);
if (0 === strpos($error['message'], 'Allowed memory') || 0 === strpos($error['message'], 'Out of memory')) {
$exception = new OutOfMemoryException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, false);
} else {
$exception = new FatalErrorException($handler->levels[$error['type']].': '.$error['message'], 0, $error['type'], $error['file'], $error['line'], 2, true);
}
} elseif (!isset($exception)) {
return;
}
try {
$handler->handleException($exception, $error);
} catch (FatalErrorException $e) {
// Ignore this re-throw
}
}

View File

@ -52,6 +52,11 @@ class FatalErrorException extends \ErrorException
unset($frame);
$trace = array_reverse($trace);
} elseif (function_exists('symfony_debug_backtrace')) {
$trace = symfony_debug_backtrace();
if (0 < $traceOffset) {
array_splice($trace, 0, $traceOffset);
}
} else {
$trace = array();
}

View File

@ -0,0 +1,133 @@
Symfony Debug Extension
=======================
This extension publishes several functions to help building powerful debugging tools.
symfony_zval_info()
-------------------
- exposes zval_hash/refcounts, allowing e.g. efficient exploration of arbitrary structures in PHP,
- does work with references, preventing memory copying.
Its behavior is about the same as:
```php
<?php
function symfony_zval_info($key, $array, $options = 0)
{
// $options is currently not used, but could be in future version.
if (!array_key_exists($key, $array)) {
return null;
}
$info = array(
'type' => gettype($array[$key]),
'zval_hash' => /* hashed memory address of $array[$key] */,
'zval_refcount' => /* internal zval refcount of $array[$key] */,
'zval_isref' => /* is_ref status of $array[$key] */,
);
switch ($info['type']) {
case 'object':
$info += array(
'object_class' => get_class($array[$key]),
'object_refcount' => /* internal object refcount of $array[$key] */,
'object_hash' => spl_object_hash($array[$key]),
'object_handle' => /* internal object handle $array[$key] */,
);
break;
case 'resource':
$info += array(
'resource_handle' => (int) $array[$key],
'resource_type' => get_resource_type($array[$key]),
'resource_refcount' => /* internal resource refcount of $array[$key] */,
);
break;
case 'array':
$info += array(
'array_count' => count($array[$key]),
);
break;
case 'string':
$info += array(
'strlen' => strlen($array[$key]),
);
break;
}
return $info;
}
```
symfony_debug_backtrace()
-------------------------
This function works like debug_backtrace(), except that it can fetch the full backtrace in case of fatal errors:
```php
function foo() { fatal(); }
function bar() { foo(); }
function sd() { var_dump(symfony_debug_backtrace()); }
register_shutdown_function('sd');
bar();
/* Will output
Fatal error: Call to undefined function fatal() in foo.php on line 42
array(3) {
[0]=>
array(2) {
["function"]=>
string(2) "sd"
["args"]=>
array(0) {
}
}
[1]=>
array(4) {
["file"]=>
string(7) "foo.php"
["line"]=>
int(1)
["function"]=>
string(3) "foo"
["args"]=>
array(0) {
}
}
[2]=>
array(4) {
["file"]=>
string(102) "foo.php"
["line"]=>
int(2)
["function"]=>
string(3) "bar"
["args"]=>
array(0) {
}
}
}
*/
```
Usage
-----
The extension is compatible with ZTS mode, and should be supported by PHP5.3, 5.4, 5.5 and 5.6.
To enable the extension from source, run:
```
phpize
./configure
make
sudo make install
```

View File

@ -1,72 +0,0 @@
Symfony Debug Extension
=======================
This extension adds a ``symfony_zval_info($key, $array, $options = 0)`` function that:
- exposes zval_hash/refcounts, allowing e.g. efficient exploration of arbitrary structures in PHP,
- does work with references, preventing memory copying.
Its behavior is about the same as:
.. code-block:: php
<?php
function symfony_zval_info($key, $array, $options = 0)
{
// $options is currently not used, but could be in future version.
if (!array_key_exists($key, $array)) {
return null;
}
$info = array(
'type' => gettype($array[$key]),
'zval_hash' => /* hashed memory address of $array[$key] */,
'zval_refcount' => /* internal zval refcount of $array[$key] */,
'zval_isref' => /* is_ref status of $array[$key] */,
);
switch ($info['type']) {
case 'object':
$info += array(
'object_class' => get_class($array[$key]),
'object_refcount' => /* internal object refcount of $array[$key] */,
'object_hash' => spl_object_hash($array[$key]),
'object_handle' => /* internal object handle $array[$key] */,
);
break;
case 'resource':
$info += array(
'resource_handle' => (int) $array[$key],
'resource_type' => get_resource_type($array[$key]),
'resource_refcount' => /* internal resource refcount of $array[$key] */,
);
break;
case 'array':
$info += array(
'array_count' => count($array[$key]),
);
break;
case 'string':
$info += array(
'strlen' => strlen($array[$key]),
);
break;
}
return $info;
}
To enable the extension from source, run:
.. code-block:: sh
phpize
./configure
make
sudo make install

View File

@ -13,7 +13,7 @@
extern zend_module_entry symfony_debug_module_entry;
#define phpext_symfony_debug_ptr &symfony_debug_module_entry
#define PHP_SYMFONY_DEBUG_VERSION "1.0"
#define PHP_SYMFONY_DEBUG_VERSION "2.7"
#ifdef PHP_WIN32
# define PHP_SYMFONY_DEBUG_API __declspec(dllexport)
@ -29,6 +29,8 @@ extern zend_module_entry symfony_debug_module_entry;
ZEND_BEGIN_MODULE_GLOBALS(symfony_debug)
intptr_t req_rand_init;
void (*old_error_cb)(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args);
zval *debug_bt;
ZEND_END_MODULE_GLOBALS(symfony_debug)
PHP_MINIT_FUNCTION(symfony_debug);
@ -40,11 +42,14 @@ PHP_GINIT_FUNCTION(symfony_debug);
PHP_GSHUTDOWN_FUNCTION(symfony_debug);
PHP_FUNCTION(symfony_zval_info);
PHP_FUNCTION(symfony_debug_backtrace);
static char *_symfony_debug_memory_address_hash(void *);
static char *_symfony_debug_memory_address_hash(void * TSRMLS_DC);
static const char *_symfony_debug_zval_type(zval *);
static const char* _symfony_debug_get_resource_type(long);
static int _symfony_debug_get_resource_refcount(long);
static const char* _symfony_debug_get_resource_type(long TSRMLS_DC);
static int _symfony_debug_get_resource_refcount(long TSRMLS_DC);
void symfony_debug_error_cb(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args);
#ifdef ZTS
#define SYMFONY_DEBUG_G(v) TSRMG(symfony_debug_globals_id, zend_symfony_debug_globals *, v)

View File

@ -12,6 +12,9 @@
#endif
#include "php.h"
#ifdef ZTS
#include "TSRM.h"
#endif
#include "php_ini.h"
#include "ext/standard/info.h"
#include "php_symfony_debug.h"
@ -19,6 +22,13 @@
#include "ext/standard/php_lcg.h"
#include "ext/spl/php_spl.h"
#include "Zend/zend_gc.h"
#include "Zend/zend_builtin_functions.h"
#include "Zend/zend_extensions.h" /* for ZEND_EXTENSION_API_NO */
#include "ext/standard/php_array.h"
#include "Zend/zend_interfaces.h"
#include "SAPI.h"
#define IS_PHP_53 ZEND_EXTENSION_API_NO == 220090626
ZEND_DECLARE_MODULE_GLOBALS(symfony_debug)
@ -30,9 +40,28 @@ ZEND_END_ARG_INFO()
const zend_function_entry symfony_debug_functions[] = {
PHP_FE(symfony_zval_info, symfony_zval_arginfo)
PHP_FE(symfony_debug_backtrace, NULL)
PHP_FE_END
};
PHP_FUNCTION(symfony_debug_backtrace)
{
if (zend_parse_parameters_none() == FAILURE) {
return;
}
#if IS_PHP_53
zend_fetch_debug_backtrace(return_value, 1, 0 TSRMLS_CC);
#else
zend_fetch_debug_backtrace(return_value, 1, 0, 0 TSRMLS_CC);
#endif
if (!SYMFONY_DEBUG_G(debug_bt)) {
return;
}
php_array_merge(Z_ARRVAL_P(return_value), Z_ARRVAL_P(SYMFONY_DEBUG_G(debug_bt)), 0 TSRMLS_CC);
}
PHP_FUNCTION(symfony_zval_info)
{
zval *key = NULL, *arg = NULL;
@ -40,7 +69,7 @@ PHP_FUNCTION(symfony_zval_info)
HashTable *array = NULL;
long options = 0;
if (zend_parse_parameters(ZEND_NUM_ARGS(), "zh|l", &key, &array, &options) == FAILURE) {
if (zend_parse_parameters(ZEND_NUM_ARGS() TSRMLS_CC, "zh|l", &key, &array, &options) == FAILURE) {
return;
}
@ -62,13 +91,14 @@ PHP_FUNCTION(symfony_zval_info)
array_init(return_value);
add_assoc_string(return_value, "type", (char *)_symfony_debug_zval_type(arg), 1);
add_assoc_stringl(return_value, "zval_hash", _symfony_debug_memory_address_hash((void *)arg), 16, 1);
add_assoc_stringl(return_value, "zval_hash", _symfony_debug_memory_address_hash((void *)arg TSRMLS_CC), 16, 0);
add_assoc_long(return_value, "zval_refcount", Z_REFCOUNT_P(arg));
add_assoc_bool(return_value, "zval_isref", (zend_bool)Z_ISREF_P(arg));
if (Z_TYPE_P(arg) == IS_OBJECT) {
static char hash[33] = {0};
php_spl_object_hash(arg, (char *)hash);
char hash[33] = {0};
php_spl_object_hash(arg, (char *)hash TSRMLS_CC);
add_assoc_stringl(return_value, "object_class", (char *)Z_OBJCE_P(arg)->name, Z_OBJCE_P(arg)->name_length, 1);
add_assoc_long(return_value, "object_refcount", EG(objects_store).object_buckets[Z_OBJ_HANDLE_P(arg)].bucket.obj.refcount);
add_assoc_string(return_value, "object_hash", hash, 1);
@ -77,17 +107,41 @@ PHP_FUNCTION(symfony_zval_info)
add_assoc_long(return_value, "array_count", zend_hash_num_elements(Z_ARRVAL_P(arg)));
} else if(Z_TYPE_P(arg) == IS_RESOURCE) {
add_assoc_long(return_value, "resource_handle", Z_LVAL_P(arg));
add_assoc_string(return_value, "resource_type", (char *)_symfony_debug_get_resource_type(Z_LVAL_P(arg)), 1);
add_assoc_long(return_value, "resource_refcount", _symfony_debug_get_resource_refcount(Z_LVAL_P(arg)));
add_assoc_string(return_value, "resource_type", (char *)_symfony_debug_get_resource_type(Z_LVAL_P(arg) TSRMLS_CC), 1);
add_assoc_long(return_value, "resource_refcount", _symfony_debug_get_resource_refcount(Z_LVAL_P(arg) TSRMLS_CC));
} else if (Z_TYPE_P(arg) == IS_STRING) {
add_assoc_long(return_value, "strlen", Z_STRLEN_P(arg));
}
}
static const char* _symfony_debug_get_resource_type(long rsid)
void symfony_debug_error_cb(int type, const char *error_filename, const uint error_lineno, const char *format, va_list args)
{
TSRMLS_FETCH();
zval *retval;
switch (type) {
case E_ERROR:
case E_PARSE:
case E_CORE_ERROR:
case E_CORE_WARNING:
case E_COMPILE_ERROR:
case E_COMPILE_WARNING:
ALLOC_INIT_ZVAL(retval);
#if IS_PHP_53
zend_fetch_debug_backtrace(retval, 1, 0 TSRMLS_CC);
#else
zend_fetch_debug_backtrace(retval, 1, 0, 0 TSRMLS_CC);
#endif
SYMFONY_DEBUG_G(debug_bt) = retval;
}
SYMFONY_DEBUG_G(old_error_cb)(type, error_filename, error_lineno, format, args);
}
static const char* _symfony_debug_get_resource_type(long rsid TSRMLS_DC)
{
const char *res_type;
res_type = zend_rsrc_list_get_rsrc_type(rsid);
res_type = zend_rsrc_list_get_rsrc_type(rsid TSRMLS_CC);
if (!res_type) {
return "Unknown";
@ -96,7 +150,7 @@ static const char* _symfony_debug_get_resource_type(long rsid)
return res_type;
}
static int _symfony_debug_get_resource_refcount(long rsid)
static int _symfony_debug_get_resource_refcount(long rsid TSRMLS_DC)
{
zend_rsrc_list_entry *le;
@ -107,21 +161,21 @@ static int _symfony_debug_get_resource_refcount(long rsid)
return 0;
}
static char *_symfony_debug_memory_address_hash(void *address)
static char *_symfony_debug_memory_address_hash(void *address TSRMLS_DC)
{
static char result[17] = {0};
char *result = NULL;
intptr_t address_rand;
if (!SYMFONY_DEBUG_G(req_rand_init)) {
if (!BG(mt_rand_is_seeded)) {
php_mt_srand(GENERATE_SEED() TSRMLS_CC);
}
SYMFONY_DEBUG_G(req_rand_init) = (intptr_t)php_mt_rand();
SYMFONY_DEBUG_G(req_rand_init) = (intptr_t)php_mt_rand(TSRMLS_C);
}
address_rand = (intptr_t)address ^ SYMFONY_DEBUG_G(req_rand_init);
snprintf(result, 17, "%016zx", address_rand);
spprintf(&result, 17, "%016zx", address_rand);
return result;
}
@ -187,7 +241,7 @@ ZEND_GET_MODULE(symfony_debug)
PHP_GINIT_FUNCTION(symfony_debug)
{
symfony_debug_globals->req_rand_init = 0;
memset(symfony_debug_globals, 0 , sizeof(*symfony_debug_globals));
}
PHP_GSHUTDOWN_FUNCTION(symfony_debug)
@ -197,11 +251,16 @@ PHP_GSHUTDOWN_FUNCTION(symfony_debug)
PHP_MINIT_FUNCTION(symfony_debug)
{
SYMFONY_DEBUG_G(old_error_cb) = zend_error_cb;
zend_error_cb = symfony_debug_error_cb;
return SUCCESS;
}
PHP_MSHUTDOWN_FUNCTION(symfony_debug)
{
zend_error_cb = SYMFONY_DEBUG_G(old_error_cb);
return SUCCESS;
}

View File

@ -3,7 +3,7 @@ Test symfony_zval_info API
--SKIPIF--
<?php if (!extension_loaded("symfony_debug")) print "skip"; ?>
--FILE--
<?php
<?php
$int = 42;
$float = 42.42;
@ -88,7 +88,7 @@ array(8) {
["object_hash"]=>
string(32) "%s"
["object_handle"]=>
int(1)
int(%d)
}
array(5) {
["type"]=>
@ -112,7 +112,7 @@ array(7) {
["zval_isref"]=>
bool(false)
["resource_handle"]=>
int(4)
int(%d)
["resource_type"]=>
string(6) "stream"
["resource_refcount"]=>

View File

@ -0,0 +1,64 @@
--TEST--
Test symfony_debug_backtrace in case of fatal error
--SKIPIF--
<?php if (!extension_loaded("symfony_debug")) print "skip"; ?>
--FILE--
<?php
function bar()
{
foo();
}
function foo()
{
notexist();
}
function bt()
{
print_r(symfony_debug_backtrace());
}
register_shutdown_function('bt');
bar();
?>
--EXPECTF--
Fatal error: Call to undefined function notexist() in %s on line %d
Array
(
[0] => Array
(
[function] => bt
[args] => Array
(
)
)
[1] => Array
(
[file] => %s
[line] => %d
[function] => foo
[args] => Array
(
)
)
[2] => Array
(
[file] => %s
[line] => %d
[function] => bar
[args] => Array
(
)
)
)

View File

@ -0,0 +1,47 @@
--TEST--
Test symfony_debug_backtrace in case of non fatal error
--SKIPIF--
<?php if (!extension_loaded("symfony_debug")) print "skip"; ?>
--FILE--
<?php
function bar()
{
bt();
}
function bt()
{
print_r(symfony_debug_backtrace());
}
bar();
?>
--EXPECTF--
Array
(
[0] => Array
(
[file] => %s
[line] => %d
[function] => bt
[args] => Array
(
)
)
[1] => Array
(
[file] => %s
[line] => %d
[function] => bar
[args] => Array
(
)
)
)

View File

@ -0,0 +1,85 @@
--TEST--
Test ErrorHandler in case of fatal error
--SKIPIF--
<?php if (!extension_loaded("symfony_debug")) print "skip"; ?>
--FILE--
<?php
namespace Psr\Log;
class LogLevel
{
const EMERGENCY = 'emergency';
const ALERT = 'alert';
const CRITICAL = 'critical';
const ERROR = 'error';
const WARNING = 'warning';
const NOTICE = 'notice';
const INFO = 'info';
const DEBUG = 'debug';
}
namespace Symfony\Component\Debug;
$dir = __DIR__.'/../../../';
require $dir.'ErrorHandler.php';
require $dir.'Exception/FatalErrorException.php';
require $dir.'Exception/UndefinedFunctionException.php';
require $dir.'FatalErrorHandler/FatalErrorHandlerInterface.php';
require $dir.'FatalErrorHandler/ClassNotFoundFatalErrorHandler.php';
require $dir.'FatalErrorHandler/UndefinedFunctionFatalErrorHandler.php';
require $dir.'FatalErrorHandler/UndefinedMethodFatalErrorHandler.php';
function bar()
{
foo();
}
function foo()
{
notexist();
}
$handler = ErrorHandler::register();
$handler->setExceptionHandler('print_r');
if (function_exists('xdebug_disable')) {
xdebug_disable();
}
bar();
?>
--EXPECTF--
Fatal error: Call to undefined function Symfony\Component\Debug\notexist() in %s on line %d
Symfony\Component\Debug\Exception\UndefinedFunctionException Object
(
[message:protected] => Attempted to call function "notexist" from namespace "Symfony\Component\Debug".
[string:Exception:private] =>
[code:protected] => 0
[file:protected] => -
[line:protected] => %d
[trace:Exception:private] => Array
(
[0] => Array
(
%A [function] => Symfony\Component\Debug\foo
%A [args] => Array
(
)
)
[1] => Array
(
%A [function] => Symfony\Component\Debug\bar
%A [args] => Array
(
)
)
%A
)
[previous:Exception:private] =>
[severity:protected] => 1
)

View File

@ -141,9 +141,9 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase
$loggers = array(
E_DEPRECATED => array(null, LogLevel::INFO),
E_USER_DEPRECATED => array(null, LogLevel::INFO),
E_NOTICE => array($logger, LogLevel::NOTICE),
E_NOTICE => array($logger, LogLevel::WARNING),
E_USER_NOTICE => array($logger, LogLevel::CRITICAL),
E_STRICT => array(null, LogLevel::NOTICE),
E_STRICT => array(null, LogLevel::WARNING),
E_WARNING => array(null, LogLevel::WARNING),
E_USER_WARNING => array(null, LogLevel::WARNING),
E_COMPILE_WARNING => array(null, LogLevel::WARNING),

View File

@ -14,6 +14,9 @@
<testsuite name="Symfony Debug Component Test Suite">
<directory>./Tests/</directory>
</testsuite>
<testsuite name="Symfony Debug Extension Test Suite">
<directory suffix=".phpt">./Resources/ext/tests/</directory>
</testsuite>
</testsuites>
<filter>

View File

@ -14,8 +14,27 @@ CHANGELOG
2.7.0
-----
* deprecated the overwriting of `AbstractType::setDefaultOptions()` in favor of overwriting `AbstractType::configureOptions()`.
* deprecated the overwriting of `AbstractTypeExtension::setDefaultOptions()` in favor of overwriting `AbstractTypeExtension::configureOptions()`.
* added option "choice_translation_domain" to ChoiceType.
* deprecated option "precision" in favor of "scale"
* deprecated the overwriting of AbstractType::setDefaultOptions() in favor of overwriting AbstractType::configureOptions().
* deprecated the overwriting of AbstractTypeExtension::setDefaultOptions() in favor of overwriting AbstractTypeExtension::configureOptions().
* added new ChoiceList interface and implementations in the Symfony\Component\Form\ChoiceList namespace
* added new ChoiceView in the Symfony\Component\Form\ChoiceList\View namespace
* choice groups are now represented by ChoiceGroupView objects in the view
* deprecated the old ChoiceList interface and implementations
* deprecated the old ChoiceView class
* added CheckboxListMapper and RadioListMapper
* deprecated ChoiceToBooleanArrayTransformer and ChoicesToBooleanArrayTransformer
* deprecated FixCheckboxInputListener and FixRadioInputListener
* deprecated the "choice_list" option of ChoiceType
* added new options to ChoiceType:
* "choices_as_values"
* "choice_loader"
* "choice_label"
* "choice_name"
* "choice_value"
* "choice_attr"
* "group_by"
2.6.2
-----

View File

@ -0,0 +1,158 @@
<?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\ChoiceList;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
/**
* A list of choices with arbitrary data types.
*
* The user of this class is responsible for assigning string values to the
* choices. Both the choices and their values are passed to the constructor.
* Each choice must have a corresponding value (with the same array key) in
* the value array.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ArrayChoiceList implements ChoiceListInterface
{
/**
* The choices in the list.
*
* @var array
*/
protected $choices = array();
/**
* The values of the choices.
*
* @var string[]
*/
protected $values = array();
/**
* The callback for creating the value for a choice.
*
* @var callable
*/
protected $valueCallback;
/**
* Creates a list with the given choices and values.
*
* The given choice array must have the same array keys as the value array.
*
* @param array $choices The selectable choices
* @param callable|null $value The callable for creating the value for a
* choice. If `null` is passed, incrementing
* integers are used as values
*/
public function __construct(array $choices, $value = null)
{
if (null !== $value && !is_callable($value)) {
throw new UnexpectedTypeException($value, 'null or callable');
}
$this->choices = $choices;
$this->values = array();
$this->valueCallback = $value;
if (null === $value) {
$i = 0;
foreach ($this->choices as $key => $choice) {
$this->values[$key] = (string) $i++;
}
} else {
foreach ($choices as $key => $choice) {
$this->values[$key] = (string) call_user_func($value, $choice);
}
}
}
/**
* {@inheritdoc}
*/
public function getChoices()
{
return $this->choices;
}
/**
* {@inheritdoc}
*/
public function getValues()
{
return $this->values;
}
/**
* {@inheritdoc}
*/
public function getChoicesForValues(array $values)
{
$choices = array();
foreach ($values as $i => $givenValue) {
foreach ($this->values as $j => $value) {
if ($value !== (string) $givenValue) {
continue;
}
$choices[$i] = $this->choices[$j];
unset($values[$i]);
if (0 === count($values)) {
break 2;
}
}
}
return $choices;
}
/**
* {@inheritdoc}
*/
public function getValuesForChoices(array $choices)
{
$values = array();
// Use the value callback to compare choices by their values, if present
if ($this->valueCallback) {
$givenValues = array();
foreach ($choices as $i => $givenChoice) {
$givenValues[$i] = (string) call_user_func($this->valueCallback, $givenChoice);
}
return array_intersect($givenValues, $this->values);
}
// Otherwise compare choices by identity
foreach ($choices as $i => $givenChoice) {
foreach ($this->choices as $j => $choice) {
if ($choice !== $givenChoice) {
continue;
}
$values[$i] = $this->values[$j];
unset($choices[$i]);
if (0 === count($choices)) {
break 2;
}
}
}
return $values;
}
}

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\ChoiceList;
use Symfony\Component\Form\Exception\InvalidArgumentException;
/**
* A list of choices that can be stored in the keys of a PHP array.
*
* PHP arrays accept only strings and integers as array keys. Other scalar types
* are cast to integers and strings according to the description of
* {@link toArrayKey()}. This implementation applies the same casting rules for
* the choices passed to the constructor and to {@link getValuesForChoices()}.
*
* By default, the choices are cast to strings and used as values. Optionally,
* you may pass custom values. The keys of the value array must match the keys
* of the choice array.
*
* Example:
*
* ```php
* $choices = array('' => 'Don\'t know', 0 => 'No', 1 => 'Yes');
* $choiceList = new ArrayKeyChoiceList(array_keys($choices));
*
* $values = $choiceList->getValues()
* // => array('', '0', '1')
*
* $selectedValues = $choiceList->getValuesForChoices(array(true));
* // => array('1')
* ```
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ArrayKeyChoiceList extends ArrayChoiceList
{
/**
* Whether the choices are used as values.
*
* @var bool
*/
private $useChoicesAsValues = false;
/**
* Casts the given choice to an array key.
*
* PHP arrays accept only strings and integers as array keys. Integer
* strings such as "42" are automatically cast to integers. The boolean
* values "true" and "false" are cast to the integers 1 and 0. Every other
* scalar value is cast to a string.
*
* @param mixed $choice The choice
*
* @return int|string The choice as PHP array key
*
* @throws InvalidArgumentException If the choice is not scalar
*/
public static function toArrayKey($choice)
{
if (!is_scalar($choice) && null !== $choice) {
throw new InvalidArgumentException(sprintf(
'The value of type "%s" cannot be converted to a valid array key.',
gettype($choice)
));
}
if (is_bool($choice) || (string) (int) $choice === (string) $choice) {
return (int) $choice;
}
return (string) $choice;
}
/**
* Creates a list with the given choices and values.
*
* The given choice array must have the same array keys as the value array.
* Each choice must be castable to an integer/string according to the
* casting rules described in {@link toArrayKey()}.
*
* If no values are given, the choices are cast to strings and used as
* values.
*
* @param array $choices The selectable choices
* @param callable $value The callable for creating the value for a
* choice. If `null` is passed, the choices are
* cast to strings and used as values
*
* @throws InvalidArgumentException If the keys of the choices don't match
* the keys of the values or if any of the
* choices is not scalar
*/
public function __construct(array $choices, $value = null)
{
$choices = array_map(array(__CLASS__, 'toArrayKey'), $choices);
if (null === $value) {
$value = function ($choice) {
return (string) $choice;
};
$this->useChoicesAsValues = true;
}
parent::__construct($choices, $value);
}
/**
* {@inheritdoc}
*/
public function getChoicesForValues(array $values)
{
if ($this->useChoicesAsValues) {
$values = array_map('strval', $values);
// If the values are identical to the choices, so we can just return
// them to improve performance a little bit
return array_map(array(__CLASS__, 'toArrayKey'), array_intersect($values, $this->values));
}
return parent::getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function getValuesForChoices(array $choices)
{
$choices = array_map(array(__CLASS__, 'toArrayKey'), $choices);
if ($this->useChoicesAsValues) {
// If the choices are identical to the values, we can just return
// them to improve performance a little bit
return array_map('strval', array_intersect($choices, $this->choices));
}
return parent::getValuesForChoices($choices);
}
}

View File

@ -0,0 +1,76 @@
<?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\ChoiceList;
/**
* A list of choices that can be selected in a choice field.
*
* A choice list assigns string values to each of a list of choices. These
* string values are displayed in the "value" attributes in HTML and submitted
* back to the server.
*
* The acceptable data types for the choices depend on the implementation.
* Values must always be strings and (within the list) free of duplicates.
*
* The choices returned by {@link getChoices()} and the values returned by
* {@link getValues()} must have the same array indices.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ChoiceListInterface
{
/**
* Returns all selectable choices.
*
* The keys of the choices correspond to the keys of the values returned by
* {@link getValues()}.
*
* @return array The selectable choices
*/
public function getChoices();
/**
* Returns the values for the choices.
*
* The keys of the values correspond to the keys of the choices returned by
* {@link getChoices()}.
*
* @return string[] The choice values
*/
public function getValues();
/**
* Returns the choices corresponding to the given values.
*
* The choices are returned with the same keys and in the same order as the
* corresponding values in the given array.
*
* @param string[] $values An array of choice values. Non-existing values in
* this array are ignored
*
* @return array An array of choices
*/
public function getChoicesForValues(array $values);
/**
* Returns the values corresponding to the given choices.
*
* The values are returned with the same keys and in the same order as the
* corresponding choices in the given array.
*
* @param array $choices An array of choices. Non-existing choices in this
* array are ignored
*
* @return string[] An array of choice values
*/
public function getValuesForChoices(array $choices);
}

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\ChoiceList\Factory;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
/**
* Caches the choice lists created by the decorated factory.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class CachingFactoryDecorator implements ChoiceListFactoryInterface
{
/**
* @var ChoiceListFactoryInterface
*/
private $decoratedFactory;
/**
* @var ChoiceListInterface[]
*/
private $lists = array();
/**
* @var ChoiceListView[]
*/
private $views = array();
/**
* Generates a SHA-256 hash for the given value.
*
* Optionally, a namespace string can be passed. Calling this method will
* the same values, but different namespaces, will return different hashes.
*
* @param mixed $value The value to hash
* @param string $namespace Optional. The namespace
*
* @return string The SHA-256 hash
*
* @internal Should not be used by user-land code.
*/
public static function generateHash($value, $namespace = '')
{
if (is_object($value)) {
$value = spl_object_hash($value);
} elseif (is_array($value)) {
array_walk_recursive($value, function (&$v) {
if (is_object($v)) {
$v = spl_object_hash($v);
}
});
}
return hash('sha256', $namespace.':'.json_encode($value));
}
/**
* Decorates the given factory.
*
* @param ChoiceListFactoryInterface $decoratedFactory The decorated factory
*/
public function __construct(ChoiceListFactoryInterface $decoratedFactory)
{
$this->decoratedFactory = $decoratedFactory;
}
/**
* Returns the decorated factory.
*
* @return ChoiceListFactoryInterface The decorated factory
*/
public function getDecoratedFactory()
{
return $this->decoratedFactory;
}
/**
* {@inheritdoc}
*/
public function createListFromChoices($choices, $value = null)
{
if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
// The value is not validated on purpose. The decorated factory may
// decide which values to accept and which not.
// We ignore the choice groups for caching. If two choice lists are
// requested with the same choices, but a different grouping, the same
// choice list is returned.
DefaultChoiceListFactory::flatten($choices, $flatChoices);
$hash = self::generateHash(array($flatChoices, $value), 'fromChoices');
if (!isset($this->lists[$hash])) {
$this->lists[$hash] = $this->decoratedFactory->createListFromChoices($choices, $value);
}
return $this->lists[$hash];
}
/**
* {@inheritdoc}
*
* @deprecated Added for backwards compatibility in Symfony 2.7, to be
* removed in Symfony 3.0.
*/
public function createListFromFlippedChoices($choices, $value = null)
{
if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
// The value is not validated on purpose. The decorated factory may
// decide which values to accept and which not.
// We ignore the choice groups for caching. If two choice lists are
// requested with the same choices, but a different grouping, the same
// choice list is returned.
DefaultChoiceListFactory::flattenFlipped($choices, $flatChoices);
$hash = self::generateHash(array($flatChoices, $value), 'fromFlippedChoices');
if (!isset($this->lists[$hash])) {
$this->lists[$hash] = $this->decoratedFactory->createListFromFlippedChoices($choices, $value);
}
return $this->lists[$hash];
}
/**
* {@inheritdoc}
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
{
$hash = self::generateHash(array($loader, $value), 'fromLoader');
if (!isset($this->lists[$hash])) {
$this->lists[$hash] = $this->decoratedFactory->createListFromLoader($loader, $value);
}
return $this->lists[$hash];
}
/**
* {@inheritdoc}
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
{
// The input is not validated on purpose. This way, the decorated
// factory may decide which input to accept and which not.
$hash = self::generateHash(array($list, $preferredChoices, $label, $index, $groupBy, $attr));
if (!isset($this->views[$hash])) {
$this->views[$hash] = $this->decoratedFactory->createView(
$list,
$preferredChoices,
$label,
$index,
$groupBy,
$attr
);
}
return $this->views[$hash];
}
}

View File

@ -0,0 +1,124 @@
<?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\ChoiceList\Factory;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
/**
* Creates {@link ChoiceListInterface} instances.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ChoiceListFactoryInterface
{
/**
* Creates a choice list for the given choices.
*
* The choices should be passed in the values of the choices array.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param array|\Traversable $choices The choices
* @param null|callable $value The callable generating the choice
* values
*
* @return ChoiceListInterface The choice list
*/
public function createListFromChoices($choices, $value = null);
/**
* Creates a choice list for the given choices.
*
* The choices should be passed in the keys of the choices array. Since the
* choices array will be flipped, the entries of the array must be strings
* or integers.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param array|\Traversable $choices The choices
* @param null|callable $value The callable generating the choice
* values
*
* @return ChoiceListInterface The choice list
*
* @deprecated Added for backwards compatibility in Symfony 2.7, to be
* removed in Symfony 3.0.
*/
public function createListFromFlippedChoices($choices, $value = null);
/**
* Creates a choice list that is loaded with the given loader.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param ChoiceLoaderInterface $loader The choice loader
* @param null|callable $value The callable generating the choice
* values
*
* @return ChoiceListInterface The choice list
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null);
/**
* Creates a view for the given choice list.
*
* Callables may be passed for all optional arguments. The callables receive
* the choice as first and the array key as the second argument.
*
* * The callable for the label and the name should return the generated
* label/choice name.
* * The callable for the preferred choices should return true or false,
* depending on whether the choice should be preferred or not.
* * The callable for the grouping should return the group name or null if
* a choice should not be grouped.
* * The callable for the attributes should return an array of HTML
* attributes that will be inserted in the tag of the choice.
*
* If no callable is passed, the labels will be generated from the choice
* keys. The view indices will be generated using an incrementing integer
* by default.
*
* The preferred choices can also be passed as array. Each choice that is
* contained in that array will be marked as preferred.
*
* The groups can be passed as a multi-dimensional array. In that case, a
* group will be created for each array entry containing a nested array.
* For all other entries, the choice for the corresponding key will be
* inserted at that position.
*
* The attributes can be passed as multi-dimensional array. The keys should
* match the keys of the choices. The values should be arrays of HTML
* attributes that should be added to the respective choice.
*
* @param ChoiceListInterface $list The choice list
* @param null|array|callable $preferredChoices The preferred choices
* @param null|callable $label The callable generating
* the choice labels
* @param null|callable $index The callable generating
* the view indices
* @param null|array|\Traversable|callable $groupBy The callable generating
* the group names
* @param null|array|callable $attr The callable generating
* the HTML attributes
*
* @return ChoiceListView The choice list view
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null);
}

View File

@ -0,0 +1,343 @@
<?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\ChoiceList\Factory;
use Symfony\Component\Form\ChoiceList\ArrayKeyChoiceList;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\LazyChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Component\Form\ChoiceList\View\ChoiceView;
use Symfony\Component\Form\Extension\Core\ChoiceList\ChoiceListInterface as LegacyChoiceListInterface;
/**
* Default implementation of {@link ChoiceListFactoryInterface}.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class DefaultChoiceListFactory implements ChoiceListFactoryInterface
{
/**
* Flattens an array into the given output variable.
*
* @param array $array The array to flatten
* @param array $output The flattened output
*
* @internal Should not be used by user-land code
*/
public static function flatten(array $array, &$output)
{
if (null === $output) {
$output = array();
}
foreach ($array as $key => $value) {
if (is_array($value)) {
self::flatten($value, $output);
continue;
}
$output[$key] = $value;
}
}
/**
* Flattens and flips an array into the given output variable.
*
* During the flattening, the keys and values of the input array are
* flipped.
*
* @param array $array The array to flatten
* @param array $output The flattened output
*
* @internal Should not be used by user-land code
*/
public static function flattenFlipped(array $array, &$output)
{
if (null === $output) {
$output = array();
}
foreach ($array as $key => $value) {
if (is_array($value)) {
self::flattenFlipped($value, $output);
continue;
}
$output[$value] = $key;
}
}
/**
* {@inheritdoc}
*/
public function createListFromChoices($choices, $value = null)
{
if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
// If the choices are given as recursive array (i.e. with explicit
// choice groups), flatten the array. The grouping information is needed
// in the view only.
self::flatten($choices, $flatChoices);
return new ArrayChoiceList($flatChoices, $value);
}
/**
* {@inheritdoc}
*
* @deprecated Added for backwards compatibility in Symfony 2.7, to be
* removed in Symfony 3.0.
*/
public function createListFromFlippedChoices($choices, $value = null)
{
if ($choices instanceof \Traversable) {
$choices = iterator_to_array($choices);
}
// If the choices are given as recursive array (i.e. with explicit
// choice groups), flatten the array. The grouping information is needed
// in the view only.
self::flattenFlipped($choices, $flatChoices);
// If no values are given, use the choices as values
// Since the choices are stored in the collection keys, i.e. they are
// strings or integers, we are guaranteed to be able to convert them
// to strings
if (null === $value) {
$value = function ($choice) {
return (string) $choice;
};
}
return new ArrayKeyChoiceList($flatChoices, $value);
}
/**
* {@inheritdoc}
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
{
return new LazyChoiceList($loader, $value);
}
/**
* {@inheritdoc}
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
{
// Backwards compatibility
if ($list instanceof LegacyChoiceListInterface && null === $preferredChoices
&& null === $label && null === $index && null === $groupBy && null === $attr) {
return new ChoiceListView($list->getRemainingViews(), $list->getPreferredViews());
}
$preferredViews = array();
$otherViews = array();
$choices = $list->getChoices();
$values = $list->getValues();
if (!is_callable($preferredChoices) && !empty($preferredChoices)) {
$preferredChoices = function ($choice) use ($preferredChoices) {
return false !== array_search($choice, $preferredChoices, true);
};
}
// The names are generated from an incrementing integer by default
if (null === $index) {
$index = 0;
}
// If $groupBy is not given, no grouping is done
if (empty($groupBy)) {
foreach ($choices as $key => $choice) {
self::addChoiceView(
$choice,
$key,
$label,
$values,
$index,
$attr,
$preferredChoices,
$preferredViews,
$otherViews
);
}
return new ChoiceListView($otherViews, $preferredViews);
}
// If $groupBy is a callable, choices are added to the group with the
// name returned by the callable. If the callable returns null, the
// choice is not added to any group
if (is_callable($groupBy)) {
foreach ($choices as $key => $choice) {
self::addChoiceViewGroupedBy(
$groupBy,
$choice,
$key,
$label,
$values,
$index,
$attr,
$preferredChoices,
$preferredViews,
$otherViews
);
}
} else {
// If $groupBy is passed as array, use that array as template for
// constructing the groups
self::addChoiceViewsGroupedBy(
$groupBy,
$label,
$choices,
$values,
$index,
$attr,
$preferredChoices,
$preferredViews,
$otherViews
);
}
// Remove any empty group view that may have been created by
// addChoiceViewGroupedBy()
foreach ($preferredViews as $key => $view) {
if ($view instanceof ChoiceGroupView && 0 === count($view->choices)) {
unset($preferredViews[$key]);
}
}
foreach ($otherViews as $key => $view) {
if ($view instanceof ChoiceGroupView && 0 === count($view->choices)) {
unset($otherViews[$key]);
}
}
return new ChoiceListView($otherViews, $preferredViews);
}
private static function addChoiceView($choice, $key, $label, $values, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews)
{
$value = $values[$key];
$nextIndex = is_int($index) ? $index++ : call_user_func($index, $choice, $key, $value);
$view = new ChoiceView(
// If the labels are null, use the choice key by default
null === $label ? (string) $key : (string) call_user_func($label, $choice, $key, $value),
$value,
$choice,
// The attributes may be a callable or a mapping from choice indices
// to nested arrays
is_callable($attr) ? call_user_func($attr, $choice, $key, $value) : (isset($attr[$key]) ? $attr[$key] : array())
);
// $isPreferred may be null if no choices are preferred
if ($isPreferred && call_user_func($isPreferred, $choice, $key, $value)) {
$preferredViews[$nextIndex] = $view;
} else {
$otherViews[$nextIndex] = $view;
}
}
private static function addChoiceViewsGroupedBy($groupBy, $label, $choices, $values, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews)
{
foreach ($groupBy as $key => $content) {
// Add the contents of groups to new ChoiceGroupView instances
if (is_array($content)) {
$preferredViewsForGroup = array();
$otherViewsForGroup = array();
self::addChoiceViewsGroupedBy(
$content,
$label,
$choices,
$values,
$index,
$attr,
$isPreferred,
$preferredViewsForGroup,
$otherViewsForGroup
);
if (count($preferredViewsForGroup) > 0) {
$preferredViews[$key] = new ChoiceGroupView($key, $preferredViewsForGroup);
}
if (count($otherViewsForGroup) > 0) {
$otherViews[$key] = new ChoiceGroupView($key, $otherViewsForGroup);
}
continue;
}
// Add ungrouped items directly
self::addChoiceView(
$choices[$key],
$key,
$label,
$values,
$index,
$attr,
$isPreferred,
$preferredViews,
$otherViews
);
}
}
private static function addChoiceViewGroupedBy($groupBy, $choice, $key, $label, $values, &$index, $attr, $isPreferred, &$preferredViews, &$otherViews)
{
$groupLabel = call_user_func($groupBy, $choice, $key, $values[$key]);
if (null === $groupLabel) {
// If the callable returns null, don't group the choice
self::addChoiceView(
$choice,
$key,
$label,
$values,
$index,
$attr,
$isPreferred,
$preferredViews,
$otherViews
);
return;
}
// Initialize the group views if necessary. Unnnecessarily built group
// views will be cleaned up at the end of createView()
if (!isset($preferredViews[$groupLabel])) {
$preferredViews[$groupLabel] = new ChoiceGroupView($groupLabel);
$otherViews[$groupLabel] = new ChoiceGroupView($groupLabel);
}
self::addChoiceView(
$choice,
$key,
$label,
$values,
$index,
$attr,
$isPreferred,
$preferredViews[$groupLabel]->choices,
$otherViews[$groupLabel]->choices
);
}
}

View File

@ -0,0 +1,230 @@
<?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\ChoiceList\Factory;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;
use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyAccess\PropertyPath;
/**
* Adds property path support to a choice list factory.
*
* Pass the decorated factory to the constructor:
*
* ```php
* $decorator = new PropertyAccessDecorator($factory);
* ```
*
* You can now pass property paths for generating choice values, labels, view
* indices, HTML attributes and for determining the preferred choices and the
* choice groups:
*
* ```php
* // extract values from the $value property
* $list = $createListFromChoices($objects, 'value');
* ```
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class PropertyAccessDecorator implements ChoiceListFactoryInterface
{
/**
* @var ChoiceListFactoryInterface
*/
private $decoratedFactory;
/**
* @var PropertyAccessorInterface
*/
private $propertyAccessor;
/**
* Decorates the given factory.
*
* @param ChoiceListFactoryInterface $decoratedFactory The decorated factory
* @param null|PropertyAccessorInterface $propertyAccessor The used property accessor
*/
public function __construct(ChoiceListFactoryInterface $decoratedFactory, PropertyAccessorInterface $propertyAccessor = null)
{
$this->decoratedFactory = $decoratedFactory;
$this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
}
/**
* Returns the decorated factory.
*
* @return ChoiceListFactoryInterface The decorated factory
*/
public function getDecoratedFactory()
{
return $this->decoratedFactory;
}
/**
* {@inheritdoc}
*
* @param array|\Traversable $choices The choices
* @param null|callable|string|PropertyPath $value The callable or path for
* generating the choice values
*
* @return ChoiceListInterface The choice list
*/
public function createListFromChoices($choices, $value = null)
{
if (is_string($value)) {
$value = new PropertyPath($value);
}
if ($value instanceof PropertyPath) {
$accessor = $this->propertyAccessor;
$value = function ($choice) use ($accessor, $value) {
// The callable may be invoked with a non-object/array value
// when such values are passed to
// ChoiceListInterface::getValuesForChoices(). Handle this case
// so that the call to getValue() doesn't break.
if (is_object($choice) || is_array($choice)) {
return $accessor->getValue($choice, $value);
}
return;
};
}
return $this->decoratedFactory->createListFromChoices($choices, $value);
}
/**
* {@inheritdoc}
*
* @param array|\Traversable $choices The choices
* @param null|callable|string|PropertyPath $value The callable or path for
* generating the choice values
*
* @return ChoiceListInterface The choice list
*
* @deprecated Added for backwards compatibility in Symfony 2.7, to be
* removed in Symfony 3.0.
*/
public function createListFromFlippedChoices($choices, $value = null)
{
// Property paths are not supported here, because array keys can never
// be objects
return $this->decoratedFactory->createListFromFlippedChoices($choices, $value);
}
/**
* {@inheritdoc}
*
* @param ChoiceLoaderInterface $loader The choice loader
* @param null|callable|string|PropertyPath $value The callable or path for
* generating the choice values
*
* @return ChoiceListInterface The choice list
*/
public function createListFromLoader(ChoiceLoaderInterface $loader, $value = null)
{
if (is_string($value)) {
$value = new PropertyPath($value);
}
if ($value instanceof PropertyPath) {
$accessor = $this->propertyAccessor;
$value = function ($choice) use ($accessor, $value) {
return $accessor->getValue($choice, $value);
};
}
return $this->decoratedFactory->createListFromLoader($loader, $value);
}
/**
* {@inheritdoc}
*
* @param ChoiceListInterface $list The choice list
* @param null|array|callable|string|PropertyPath $preferredChoices The preferred choices
* @param null|callable|string|PropertyPath $label The callable or path generating the choice labels
* @param null|callable|string|PropertyPath $index The callable or path generating the view indices
* @param null|array|\Traversable|callable|string|PropertyPath $groupBy The callable or path generating the group names
* @param null|array|callable|string|PropertyPath $attr The callable or path generating the HTML attributes
*
* @return ChoiceListView The choice list view
*/
public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, $index = null, $groupBy = null, $attr = null)
{
$accessor = $this->propertyAccessor;
if (is_string($label)) {
$label = new PropertyPath($label);
}
if ($label instanceof PropertyPath) {
$label = function ($choice) use ($accessor, $label) {
return $accessor->getValue($choice, $label);
};
}
if (is_string($preferredChoices)) {
$preferredChoices = new PropertyPath($preferredChoices);
}
if ($preferredChoices instanceof PropertyPath) {
$preferredChoices = function ($choice) use ($accessor, $preferredChoices) {
try {
return $accessor->getValue($choice, $preferredChoices);
} catch (UnexpectedTypeException $e) {
// Assume not preferred if not readable
return false;
}
};
}
if (is_string($index)) {
$index = new PropertyPath($index);
}
if ($index instanceof PropertyPath) {
$index = function ($choice) use ($accessor, $index) {
return $accessor->getValue($choice, $index);
};
}
if (is_string($groupBy)) {
$groupBy = new PropertyPath($groupBy);
}
if ($groupBy instanceof PropertyPath) {
$groupBy = function ($choice) use ($accessor, $groupBy) {
try {
return $accessor->getValue($choice, $groupBy);
} catch (UnexpectedTypeException $e) {
// Don't group if path is not readable
}
};
}
if (is_string($attr)) {
$attr = new PropertyPath($attr);
}
if ($attr instanceof PropertyPath) {
$attr = function ($choice) use ($accessor, $attr) {
return $accessor->getValue($choice, $attr);
};
}
return $this->decoratedFactory->createView($list, $preferredChoices, $label, $index, $groupBy, $attr);
}
}

View File

@ -0,0 +1,123 @@
<?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\ChoiceList;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
/**
* A choice list that loads its choices lazily.
*
* The choices are fetched using a {@link ChoiceLoaderInterface} instance.
* If only {@link getChoicesForValues()} or {@link getValuesForChoices()} is
* called, the choice list is only loaded partially for improved performance.
*
* Once {@link getChoices()} or {@link getValues()} is called, the list is
* loaded fully.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class LazyChoiceList implements ChoiceListInterface
{
/**
* The choice loader.
*
* @var ChoiceLoaderInterface
*/
private $loader;
/**
* The callable creating string values for each choice.
*
* If null, choices are simply cast to strings.
*
* @var null|callable
*/
private $value;
/**
* Whether to use the value callback to compare choices.
*
* @var bool
*/
private $compareByValue;
/**
* @var ChoiceListInterface|null
*/
private $loadedList;
/**
* Creates a lazily-loaded list using the given loader.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param ChoiceLoaderInterface $loader The choice loader
* @param null|callable $value The callable generating the choice
* values
*/
public function __construct(ChoiceLoaderInterface $loader, $value = null, $compareByValue = false)
{
$this->loader = $loader;
$this->value = $value;
$this->compareByValue = $compareByValue;
}
/**
* {@inheritdoc}
*/
public function getChoices()
{
if (!$this->loadedList) {
$this->loadedList = $this->loader->loadChoiceList($this->value);
}
return $this->loadedList->getChoices();
}
/**
* {@inheritdoc}
*/
public function getValues()
{
if (!$this->loadedList) {
$this->loadedList = $this->loader->loadChoiceList($this->value);
}
return $this->loadedList->getValues();
}
/**
* {@inheritdoc}
*/
public function getChoicesForValues(array $values)
{
if (!$this->loadedList) {
return $this->loader->loadChoicesForValues($values, $this->value);
}
return $this->loadedList->getChoicesForValues($values);
}
/**
* {@inheritdoc}
*/
public function getValuesForChoices(array $choices)
{
if (!$this->loadedList) {
return $this->loader->loadValuesForChoices($choices, $this->value);
}
return $this->loadedList->getValuesForChoices($choices);
}
}

View File

@ -0,0 +1,76 @@
<?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\ChoiceList\Loader;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
/**
* Loads a choice list.
*
* The methods {@link loadChoicesForValues()} and {@link loadValuesForChoices()}
* can be used to load the list only partially in cases where a fully-loaded
* list is not necessary.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
interface ChoiceLoaderInterface
{
/**
* Loads a list of choices.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param null|callable $value The callable which generates the values
* from choices
*
* @return ChoiceListInterface The loaded choice list
*/
public function loadChoiceList($value = null);
/**
* Loads the choices corresponding to the given values.
*
* The choices are returned with the same keys and in the same order as the
* corresponding values in the given array.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param string[] $values An array of choice values. Non-existing
* values in this array are ignored
* @param null|callable $value The callable generating the choice values
*
* @return array An array of choices
*/
public function loadChoicesForValues(array $values, $value = null);
/**
* Loads the values corresponding to the given choices.
*
* The values are returned with the same keys and in the same order as the
* corresponding choices in the given array.
*
* Optionally, a callable can be passed for generating the choice values.
* The callable receives the choice as first and the array key as the second
* argument.
*
* @param array $choices An array of choices. Non-existing choices in
* this array are ignored
* @param null|callable $value The callable generating the choice values
*
* @return string[] An array of choice values
*/
public function loadValuesForChoices(array $choices, $value = null);
}

View File

@ -0,0 +1,55 @@
<?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\ChoiceList\View;
/**
* Represents a group of choices in templates.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceGroupView implements \IteratorAggregate
{
/**
* The label of the group
*
* @var string
*/
public $label;
/**
* The choice views in the group
*
* @var ChoiceGroupView[]|ChoiceView[]
*/
public $choices;
/**
* Creates a new choice group view.
*
* @param string $label The label of the group.
* @param ChoiceGroupView[]|ChoiceView[] $choices The choice views in the
* group.
*/
public function __construct($label, array $choices = array())
{
$this->label = $label;
$this->choices = $choices;
}
/**
* {@inheritdoc}
*/
public function getIterator()
{
return new \ArrayIterator($this->choices);
}
}

View File

@ -0,0 +1,51 @@
<?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\ChoiceList\View;
/**
* Represents a choice list in templates.
*
* A choice list contains choices and optionally preferred choices which are
* displayed in the very beginning of the list. Both choices and preferred
* choices may be grouped in {@link ChoiceGroupView} instances.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceListView
{
/**
* The choices.
*
* @var ChoiceGroupView[]|ChoiceView[]
*/
public $choices;
/**
* The preferred choices.
*
* @var ChoiceGroupView[]|ChoiceView[]
*/
public $preferredChoices;
/**
* Creates a new choice list view.
*
* @param ChoiceGroupView[]|ChoiceView[] $choices The choice views.
* @param ChoiceGroupView[]|ChoiceView[] $preferredChoices The preferred
* choice views.
*/
public function __construct(array $choices = array(), array $preferredChoices = array())
{
$this->choices = $choices;
$this->preferredChoices = $preferredChoices;
}
}

View File

@ -0,0 +1,64 @@
<?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\ChoiceList\View;
/**
* Represents a choice in templates.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*/
class ChoiceView
{
/**
* The label displayed to humans.
*
* @var string
*/
public $label;
/**
* The view representation of the choice.
*
* @var string
*/
public $value;
/**
* The original choice value.
*
* @var mixed
*/
public $data;
/**
* Additional attributes for the HTML tag.
*
* @var array
*/
public $attr;
/**
* Creates a new choice view.
*
* @param string $label The label displayed to humans
* @param string $value The view representation of the choice
* @param mixed $data The original choice
* @param array $attr Additional attributes for the HTML tag
*/
public function __construct($label, $value, $data, array $attr = array())
{
$this->label = $label;
$this->value = $value;
$this->data = $data;
$this->attr = $attr;
}
}

View File

@ -29,10 +29,13 @@ use Symfony\Component\Form\Extension\Core\View\ChoiceView;
* <code>
* $choices = array(true, false);
* $labels = array('Agree', 'Disagree');
* $choiceList = new ChoiceList($choices, $labels);
* $choiceList = new ArrayChoiceList($choices, $labels);
* </code>
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
* Use {@link \Symfony\Component\Form\ChoiceList\ArrayChoiceList} instead.
*/
class ChoiceList implements ChoiceListInterface
{
@ -89,6 +92,8 @@ class ChoiceList implements ChoiceListInterface
}
$this->initialize($choices, $labels, $preferredChoices);
trigger_error('The '.__CLASS__.' class is deprecated since version 2.7 and will be removed in 3.0. Use Symfony\Component\Form\ChoiceList\ArrayChoiceList instead.', E_USER_DEPRECATED);
}
/**

View File

@ -11,6 +11,8 @@
namespace Symfony\Component\Form\Extension\Core\ChoiceList;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface as BaseChoiceListInterface;
/**
* Contains choices that can be selected in a form field.
*
@ -25,23 +27,12 @@ namespace Symfony\Component\Form\Extension\Core\ChoiceList;
* in the HTML "value" attribute.
*
* @author Bernhard Schussek <bschussek@gmail.com>
*
* @deprecated Deprecated since Symfony 2.7, to be removed in Symfony 3.0.
* Use {@link BaseChoiceListInterface} instead.
*/
interface ChoiceListInterface
interface ChoiceListInterface extends BaseChoiceListInterface
{
/**
* Returns the list of choices.
*
* @return array The choices with their indices as keys
*/
public function getChoices();
/**
* Returns the values for the choices.
*
* @return array The values with the corresponding choice indices as keys
*/
public function getValues();
/**
* Returns the choice views of the preferred choices as nested array with
* the choice groups as top-level keys.
@ -92,37 +83,6 @@ interface ChoiceListInterface
*/
public function getRemainingViews();
/**
* Returns the choices corresponding to the given values.
*
* The choices can have any data type.
*
* The choices must be returned with the same keys and in the same order
* as the corresponding values in the given array.
*
* @param array $values An array of choice values. Not existing values in
* this array are ignored
*
* @return array An array of choices with ascending, 0-based numeric keys
*/
public function getChoicesForValues(array $values);
/**
* Returns the values corresponding to the given choices.
*
* The values must be strings.
*
* The values must be returned with the same keys and in the same order
* as the corresponding choices in the given array.
*
* @param array $choices An array of choices. Not existing choices in this
* array are ignored
*
* @return array An array of choice values with ascending, 0-based numeric
* keys
*/
public function getValuesForChoices(array $choices);
/**
* Returns the indices corresponding to the given choices.
*

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