diff --git a/src/Symfony/Components/File/Exception/AccessDeniedException.php b/src/Symfony/Components/File/Exception/AccessDeniedException.php new file mode 100644 index 000000000000..eb8a3a4d7ba0 --- /dev/null +++ b/src/Symfony/Components/File/Exception/AccessDeniedException.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Thrown when the access on a file was denied. + * + * @author Bernhard Schussek + */ +class AccessDeniedException extends FileException +{ + /** + * Constructor. + * + * @param string $path The path to the accessed file + */ + public function __construct($path) + { + parent::__construct(sprintf('The file %s could not be accessed', $path)); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/File/Exception/FileException.php b/src/Symfony/Components/File/Exception/FileException.php new file mode 100644 index 000000000000..d1980db2cb3d --- /dev/null +++ b/src/Symfony/Components/File/Exception/FileException.php @@ -0,0 +1,20 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Thrown when an error occurred in the component File + * + * @author Bernhard Schussek + */ +class FileException extends \RuntimeException +{ +} \ No newline at end of file diff --git a/src/Symfony/Components/File/Exception/FileNotFoundException.php b/src/Symfony/Components/File/Exception/FileNotFoundException.php new file mode 100644 index 000000000000..b5cfd1c92caf --- /dev/null +++ b/src/Symfony/Components/File/Exception/FileNotFoundException.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Thrown when a file was not found + * + * @author Bernhard Schussek + */ +class FileNotFoundException extends FileException +{ + /** + * Constructor. + * + * @param string $path The path to the file that was not found + */ + public function __construct($path) + { + parent::__construct(sprintf('The file %s does not exist', $path)); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/File/File.php b/src/Symfony/Components/File/File.php new file mode 100644 index 000000000000..719c33966ee1 --- /dev/null +++ b/src/Symfony/Components/File/File.php @@ -0,0 +1,586 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A file in the file system + * + * @author Bernhard Schussek + */ +class File +{ + /** + * Assignment of mime types to their default extensions + * @var array + */ + static protected $defaultExtensions = array( + 'application/andrew-inset' => 'ez', + 'application/appledouble' => 'base64', + 'application/applefile' => 'base64', + 'application/commonground' => 'dp', + 'application/cprplayer' => 'pqi', + 'application/dsptype' => 'tsp', + 'application/excel' => 'xls', + 'application/font-tdpfr' => 'pfr', + 'application/futuresplash' => 'spl', + 'application/hstu' => 'stk', + 'application/hyperstudio' => 'stk', + 'application/javascript' => 'js', + 'application/mac-binhex40' => 'hqx', + 'application/mac-compactpro' => 'cpt', + 'application/mbed' => 'mbd', + 'application/mirage' => 'mfp', + 'application/msword' => 'doc', + 'application/ocsp-request' => 'orq', + 'application/ocsp-response' => 'ors', + 'application/octet-stream' => 'bin', + 'application/oda' => 'oda', + 'application/ogg' => 'ogg', + 'application/pdf' => 'pdf', + 'application/x-pdf' => 'pdf', + 'application/pgp-encrypted' => '7bit', + 'application/pgp-keys' => '7bit', + 'application/pgp-signature' => 'sig', + 'application/pkcs10' => 'p10', + 'application/pkcs7-mime' => 'p7m', + 'application/pkcs7-signature' => 'p7s', + 'application/pkix-cert' => 'cer', + 'application/pkix-crl' => 'crl', + 'application/pkix-pkipath' => 'pkipath', + 'application/pkixcmp' => 'pki', + 'application/postscript' => 'ps', + 'application/presentations' => 'shw', + 'application/prs.cww' => 'cw', + 'application/prs.nprend' => 'rnd', + 'application/quest' => 'qrt', + 'application/rtf' => 'rtf', + 'application/sgml-open-catalog' => 'soc', + 'application/sieve' => 'siv', + 'application/smil' => 'smi', + 'application/toolbook' => 'tbk', + 'application/vnd.3gpp.pic-bw-large' => 'plb', + 'application/vnd.3gpp.pic-bw-small' => 'psb', + 'application/vnd.3gpp.pic-bw-var' => 'pvb', + 'application/vnd.3gpp.sms' => 'sms', + 'application/vnd.acucorp' => 'atc', + 'application/vnd.adobe.xfdf' => 'xfdf', + 'application/vnd.amiga.amu' => 'ami', + 'application/vnd.blueice.multipass' => 'mpm', + 'application/vnd.cinderella' => 'cdy', + 'application/vnd.cosmocaller' => 'cmc', + 'application/vnd.criticaltools.wbs+xml' => 'wbs', + 'application/vnd.curl' => 'curl', + 'application/vnd.data-vision.rdz' => 'rdz', + 'application/vnd.dreamfactory' => 'dfac', + 'application/vnd.fsc.weblauch' => 'fsc', + 'application/vnd.genomatix.tuxedo' => 'txd', + 'application/vnd.hbci' => 'hbci', + 'application/vnd.hhe.lesson-player' => 'les', + 'application/vnd.hp-hpgl' => 'plt', + 'application/vnd.ibm.electronic-media' => 'emm', + 'application/vnd.ibm.rights-management' => 'irm', + 'application/vnd.ibm.secure-container' => 'sc', + 'application/vnd.ipunplugged.rcprofile' => 'rcprofile', + 'application/vnd.irepository.package+xml' => 'irp', + 'application/vnd.jisp' => 'jisp', + 'application/vnd.kde.karbon' => 'karbon', + 'application/vnd.kde.kchart' => 'chrt', + 'application/vnd.kde.kformula' => 'kfo', + 'application/vnd.kde.kivio' => 'flw', + 'application/vnd.kde.kontour' => 'kon', + 'application/vnd.kde.kpresenter' => 'kpr', + 'application/vnd.kde.kspread' => 'ksp', + 'application/vnd.kde.kword' => 'kwd', + 'application/vnd.kenameapp' => 'htke', + 'application/vnd.kidspiration' => 'kia', + 'application/vnd.kinar' => 'kne', + 'application/vnd.llamagraphics.life-balance.desktop' => 'lbd', + 'application/vnd.llamagraphics.life-balance.exchange+xml' => 'lbe', + 'application/vnd.lotus-1-2-3' => 'wks', + 'application/vnd.mcd' => 'mcd', + 'application/vnd.mfmp' => 'mfm', + 'application/vnd.micrografx.flo' => 'flo', + 'application/vnd.micrografx.igx' => 'igx', + 'application/vnd.mif' => 'mif', + 'application/vnd.mophun.application' => 'mpn', + 'application/vnd.mophun.certificate' => 'mpc', + 'application/vnd.mozilla.xul+xml' => 'xul', + 'application/vnd.ms-artgalry' => 'cil', + 'application/vnd.ms-asf' => 'asf', + 'application/vnd.ms-excel' => 'xls', + 'application/vnd.ms-excel.sheet.macroenabled.12' => 'xlsm', + 'application/vnd.ms-lrm' => 'lrm', + 'application/vnd.ms-powerpoint' => 'ppt', + 'application/vnd.ms-project' => 'mpp', + 'application/vnd.ms-tnef' => 'base64', + 'application/vnd.ms-works' => 'base64', + 'application/vnd.ms-wpl' => 'wpl', + 'application/vnd.mseq' => 'mseq', + 'application/vnd.nervana' => 'ent', + 'application/vnd.nokia.radio-preset' => 'rpst', + 'application/vnd.nokia.radio-presets' => 'rpss', + 'application/vnd.oasis.opendocument.text' => 'odt', + 'application/vnd.oasis.opendocument.text-template' => 'ott', + 'application/vnd.oasis.opendocument.text-web' => 'oth', + 'application/vnd.oasis.opendocument.text-master' => 'odm', + 'application/vnd.oasis.opendocument.graphics' => 'odg', + 'application/vnd.oasis.opendocument.graphics-template' => 'otg', + 'application/vnd.oasis.opendocument.presentation' => 'odp', + 'application/vnd.oasis.opendocument.presentation-template' => 'otp', + 'application/vnd.oasis.opendocument.spreadsheet' => 'ods', + 'application/vnd.oasis.opendocument.spreadsheet-template' => 'ots', + 'application/vnd.oasis.opendocument.chart' => 'odc', + 'application/vnd.oasis.opendocument.formula' => 'odf', + 'application/vnd.oasis.opendocument.database' => 'odb', + 'application/vnd.oasis.opendocument.image' => 'odi', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx', + 'application/vnd.openxmlformats-officedocument.wordprocessingml.template' => 'dotx', + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx', + 'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx', + 'application/vnd.palm' => 'prc', + 'application/vnd.picsel' => 'efif', + 'application/vnd.pvi.ptid1' => 'pti', + 'application/vnd.quark.quarkxpress' => 'qxd', + 'application/vnd.sealed.doc' => 'sdoc', + 'application/vnd.sealed.eml' => 'seml', + 'application/vnd.sealed.mht' => 'smht', + 'application/vnd.sealed.ppt' => 'sppt', + 'application/vnd.sealed.xls' => 'sxls', + 'application/vnd.sealedmedia.softseal.html' => 'stml', + 'application/vnd.sealedmedia.softseal.pdf' => 'spdf', + 'application/vnd.seemail' => 'see', + 'application/vnd.smaf' => 'mmf', + 'application/vnd.sun.xml.calc' => 'sxc', + 'application/vnd.sun.xml.calc.template' => 'stc', + 'application/vnd.sun.xml.draw' => 'sxd', + 'application/vnd.sun.xml.draw.template' => 'std', + 'application/vnd.sun.xml.impress' => 'sxi', + 'application/vnd.sun.xml.impress.template' => 'sti', + 'application/vnd.sun.xml.math' => 'sxm', + 'application/vnd.sun.xml.writer' => 'sxw', + 'application/vnd.sun.xml.writer.global' => 'sxg', + 'application/vnd.sun.xml.writer.template' => 'stw', + 'application/vnd.sus-calendar' => 'sus', + 'application/vnd.vidsoft.vidconference' => 'vsc', + 'application/vnd.visio' => 'vsd', + 'application/vnd.visionary' => 'vis', + 'application/vnd.wap.sic' => 'sic', + 'application/vnd.wap.slc' => 'slc', + 'application/vnd.wap.wbxml' => 'wbxml', + 'application/vnd.wap.wmlc' => 'wmlc', + 'application/vnd.wap.wmlscriptc' => 'wmlsc', + 'application/vnd.webturbo' => 'wtb', + 'application/vnd.wordperfect' => 'wpd', + 'application/vnd.wqd' => 'wqd', + 'application/vnd.wv.csp+wbxml' => 'wv', + 'application/vnd.wv.csp+xml' => '8bit', + 'application/vnd.wv.ssp+xml' => '8bit', + 'application/vnd.yamaha.hv-dic' => 'hvd', + 'application/vnd.yamaha.hv-script' => 'hvs', + 'application/vnd.yamaha.hv-voice' => 'hvp', + 'application/vnd.yamaha.smaf-audio' => 'saf', + 'application/vnd.yamaha.smaf-phrase' => 'spf', + 'application/vocaltec-media-desc' => 'vmd', + 'application/vocaltec-media-file' => 'vmf', + 'application/vocaltec-talker' => 'vtk', + 'application/watcherinfo+xml' => 'wif', + 'application/wordperfect5.1' => 'wp5', + 'application/x-123' => 'wk', + 'application/x-7th_level_event' => '7ls', + 'application/x-authorware-bin' => 'aab', + 'application/x-authorware-map' => 'aam', + 'application/x-authorware-seg' => 'aas', + 'application/x-bcpio' => 'bcpio', + 'application/x-bleeper' => 'bleep', + 'application/x-bzip2' => 'bz2', + 'application/x-cdlink' => 'vcd', + 'application/x-chat' => 'chat', + 'application/x-chess-pgn' => 'pgn', + 'application/x-compress' => 'z', + 'application/x-cpio' => 'cpio', + 'application/x-cprplayer' => 'pqf', + 'application/x-csh' => 'csh', + 'application/x-cu-seeme' => 'csm', + 'application/x-cult3d-object' => 'co', + 'application/x-debian-package' => 'deb', + 'application/x-director' => 'dcr', + 'application/x-dvi' => 'dvi', + 'application/x-envoy' => 'evy', + 'application/x-futuresplash' => 'spl', + 'application/x-gtar' => 'gtar', + 'application/x-gzip' => 'gz', + 'application/x-hdf' => 'hdf', + 'application/x-hep' => 'hep', + 'application/x-html+ruby' => 'rhtml', + 'application/x-httpd-miva' => 'mv', + 'application/x-httpd-php' => 'phtml', + 'application/x-ica' => 'ica', + 'application/x-imagemap' => 'imagemap', + 'application/x-ipix' => 'ipx', + 'application/x-ipscript' => 'ips', + 'application/x-java-archive' => 'jar', + 'application/x-java-jnlp-file' => 'jnlp', + 'application/x-java-serialized-object' => 'ser', + 'application/x-java-vm' => 'class', + 'application/x-javascript' => 'js', + 'application/x-koan' => 'skp', + 'application/x-latex' => 'latex', + 'application/x-mac-compactpro' => 'cpt', + 'application/x-maker' => 'frm', + 'application/x-mathcad' => 'mcd', + 'application/x-midi' => 'mid', + 'application/x-mif' => 'mif', + 'application/x-msaccess' => 'mda', + 'application/x-msdos-program' => 'com', + 'application/x-msdownload' => 'base64', + 'application/x-msexcel' => 'xls', + 'application/x-msword' => 'doc', + 'application/x-netcdf' => 'nc', + 'application/x-ns-proxy-autoconfig' => 'pac', + 'application/x-pagemaker' => 'pm5', + 'application/x-perl' => 'pl', + 'application/x-pn-realmedia' => 'rp', + 'application/x-python' => 'py', + 'application/x-quicktimeplayer' => 'qtl', + 'application/x-rar-compressed' => 'rar', + 'application/x-ruby' => 'rb', + 'application/x-sh' => 'sh', + 'application/x-shar' => 'shar', + 'application/x-shockwave-flash' => 'swf', + 'application/x-sprite' => 'spr', + 'application/x-spss' => 'sav', + 'application/x-spt' => 'spt', + 'application/x-stuffit' => 'sit', + 'application/x-sv4cpio' => 'sv4cpio', + 'application/x-sv4crc' => 'sv4crc', + 'application/x-tar' => 'tar', + 'application/x-tcl' => 'tcl', + 'application/x-tex' => 'tex', + 'application/x-texinfo' => 'texinfo', + 'application/x-troff' => 't', + 'application/x-troff-man' => 'man', + 'application/x-troff-me' => 'me', + 'application/x-troff-ms' => 'ms', + 'application/x-twinvq' => 'vqf', + 'application/x-twinvq-plugin' => 'vqe', + 'application/x-ustar' => 'ustar', + 'application/x-vmsbackup' => 'bck', + 'application/x-wais-source' => 'src', + 'application/x-wingz' => 'wz', + 'application/x-word' => 'base64', + 'application/x-wordperfect6.1' => 'wp6', + 'application/x-x509-ca-cert' => 'crt', + 'application/x-zip-compressed' => 'zip', + 'application/xhtml+xml' => 'xhtml', + 'application/zip' => 'zip', + 'audio/3gpp' => '3gpp', + 'audio/amr' => 'amr', + 'audio/amr-wb' => 'awb', + 'audio/basic' => 'au', + 'audio/evrc' => 'evc', + 'audio/l16' => 'l16', + 'audio/midi' => 'mid', + 'audio/mpeg' => 'mp3', + 'audio/prs.sid' => 'sid', + 'audio/qcelp' => 'qcp', + 'audio/smv' => 'smv', + 'audio/vnd.audiokoz' => 'koz', + 'audio/vnd.digital-winds' => 'eol', + 'audio/vnd.everad.plj' => 'plj', + 'audio/vnd.lucent.voice' => 'lvp', + 'audio/vnd.nokia.mobile-xmf' => 'mxmf', + 'audio/vnd.nortel.vbk' => 'vbk', + 'audio/vnd.nuera.ecelp4800' => 'ecelp4800', + 'audio/vnd.nuera.ecelp7470' => 'ecelp7470', + 'audio/vnd.nuera.ecelp9600' => 'ecelp9600', + 'audio/vnd.sealedmedia.softseal.mpeg' => 'smp3', + 'audio/voxware' => 'vox', + 'audio/x-aiff' => 'aif', + 'audio/x-mid' => 'mid', + 'audio/x-midi' => 'mid', + 'audio/x-mpeg' => 'mp2', + 'audio/x-mpegurl' => 'mpu', + 'audio/x-pn-realaudio' => 'rm', + 'audio/x-pn-realaudio-plugin' => 'rpm', + 'audio/x-realaudio' => 'ra', + 'audio/x-wav' => 'wav', + 'chemical/x-csml' => 'csm', + 'chemical/x-embl-dl-nucleotide' => 'emb', + 'chemical/x-gaussian-cube' => 'cube', + 'chemical/x-gaussian-input' => 'gau', + 'chemical/x-jcamp-dx' => 'jdx', + 'chemical/x-mdl-molfile' => 'mol', + 'chemical/x-mdl-rxnfile' => 'rxn', + 'chemical/x-mdl-tgf' => 'tgf', + 'chemical/x-mopac-input' => 'mop', + 'chemical/x-pdb' => 'pdb', + 'chemical/x-rasmol' => 'scr', + 'chemical/x-xyz' => 'xyz', + 'drawing/dwf' => 'dwf', + 'drawing/x-dwf' => 'dwf', + 'i-world/i-vrml' => 'ivr', + 'image/bmp' => 'bmp', + 'image/cewavelet' => 'wif', + 'image/cis-cod' => 'cod', + 'image/fif' => 'fif', + 'image/gif' => 'gif', + 'image/ief' => 'ief', + 'image/jp2' => 'jp2', + 'image/jpeg' => 'jpg', + 'image/jpm' => 'jpm', + 'image/jpx' => 'jpf', + 'image/pict' => 'pic', + 'image/pjpeg' => 'jpg', + 'image/png' => 'png', + 'image/targa' => 'tga', + 'image/tiff' => 'tif', + 'image/vn-svf' => 'svf', + 'image/vnd.dgn' => 'dgn', + 'image/vnd.djvu' => 'djvu', + 'image/vnd.dwg' => 'dwg', + 'image/vnd.glocalgraphics.pgb' => 'pgb', + 'image/vnd.microsoft.icon' => 'ico', + 'image/vnd.ms-modi' => 'mdi', + 'image/vnd.sealed.png' => 'spng', + 'image/vnd.sealedmedia.softseal.gif' => 'sgif', + 'image/vnd.sealedmedia.softseal.jpg' => 'sjpg', + 'image/vnd.wap.wbmp' => 'wbmp', + 'image/x-bmp' => 'bmp', + 'image/x-cmu-raster' => 'ras', + 'image/x-freehand' => 'fh4', + 'image/x-png' => 'png', + 'image/x-portable-anymap' => 'pnm', + 'image/x-portable-bitmap' => 'pbm', + 'image/x-portable-graymap' => 'pgm', + 'image/x-portable-pixmap' => 'ppm', + 'image/x-rgb' => 'rgb', + 'image/x-xbitmap' => 'xbm', + 'image/x-xpixmap' => 'xpm', + 'image/x-xwindowdump' => 'xwd', + 'message/external-body' => '8bit', + 'message/news' => '8bit', + 'message/partial' => '8bit', + 'message/rfc822' => '8bit', + 'model/iges' => 'igs', + 'model/mesh' => 'msh', + 'model/vnd.parasolid.transmit.binary' => 'x_b', + 'model/vnd.parasolid.transmit.text' => 'x_t', + 'model/vrml' => 'wrl', + 'multipart/alternative' => '8bit', + 'multipart/appledouble' => '8bit', + 'multipart/digest' => '8bit', + 'multipart/mixed' => '8bit', + 'multipart/parallel' => '8bit', + 'text/comma-separated-values' => 'csv', + 'text/css' => 'css', + 'text/html' => 'html', + 'text/plain' => 'txt', + 'text/prs.fallenstein.rst' => 'rst', + 'text/richtext' => 'rtx', + 'text/rtf' => 'rtf', + 'text/sgml' => 'sgml', + 'text/tab-separated-values' => 'tsv', + 'text/vnd.net2phone.commcenter.command' => 'ccc', + 'text/vnd.sun.j2me.app-descriptor' => 'jad', + 'text/vnd.wap.si' => 'si', + 'text/vnd.wap.sl' => 'sl', + 'text/vnd.wap.wml' => 'wml', + 'text/vnd.wap.wmlscript' => 'wmls', + 'text/x-hdml' => 'hdml', + 'text/x-setext' => 'etx', + 'text/x-sgml' => 'sgml', + 'text/x-speech' => 'talk', + 'text/x-vcalendar' => 'vcs', + 'text/x-vcard' => 'vcf', + 'text/xml' => 'xml', + 'ulead/vrml' => 'uvr', + 'video/3gpp' => '3gp', + 'video/dl' => 'dl', + 'video/gl' => 'gl', + 'video/mj2' => 'mj2', + 'video/mpeg' => 'mpeg', + 'video/quicktime' => 'mov', + 'video/vdo' => 'vdo', + 'video/vivo' => 'viv', + 'video/vnd.fvt' => 'fvt', + 'video/vnd.mpegurl' => 'mxu', + 'video/vnd.nokia.interleaved-multimedia' => 'nim', + 'video/vnd.objectvideo' => 'mp4', + 'video/vnd.sealed.mpeg1' => 's11', + 'video/vnd.sealed.mpeg4' => 'smpg', + 'video/vnd.sealed.swf' => 'sswf', + 'video/vnd.sealedmedia.softseal.mov' => 'smov', + 'video/vnd.vivo' => 'vivo', + 'video/x-fli' => 'fli', + 'video/x-ms-asf' => 'asf', + 'video/x-ms-wmv' => 'wmv', + 'video/x-msvideo' => 'avi', + 'video/x-sgi-movie' => 'movie', + 'x-chemical/x-pdb' => 'pdb', + 'x-chemical/x-xyz' => 'xyz', + 'x-conference/x-cooltalk' => 'ice', + 'x-drawing/dwf' => 'dwf', + 'x-world/x-d96' => 'd', + 'x-world/x-svr' => 'svr', + 'x-world/x-vream' => 'vrw', + 'x-world/x-vrml' => 'wrl', + ); + + /** + * The absolute path to the file without dots + * @var string + */ + protected $path; + + /** + * Constructs a new file from the given path. + * + * @param string $path The path to the file + * @throws FileNotFoundException If the given path is no file + */ + public function __construct($path) + { + if (!is_file($path)) + { + throw new FileNotFoundException($path); + } + + $this->path = realpath($path); + } + + /** + * Alias for getPath() + * + * @return string + */ + public function __toString() + { + return $this->getPath(); + } + + /** + * Returns the file name + * + * @return string + */ + public function getName() + { + return basename($this->path); + } + + /** + * Returns the file extension (with dot) + * + * @return string + */ + public function getExtension() + { + $name = $this->getName(); + + if (false !== ($pos = strrpos($name, '.'))) + { + return substr($name, $pos); + } + else + { + return ''; + } + } + + /** + * Returns the extension based on the mime type (with dot) + * + * If the mime type is unknown, the actual extension is returned instead. + * + * @return string + */ + public function getDefaultExtension() + { + $type = $this->getMimeType(); + + if (isset(self::$defaultExtensions[$type])) + { + return '.' . self::$defaultExtensions[$type]; + } + else + { + return $this->getExtension(); + } + } + + /** + * Returns the directory of the file + * + * @return string + */ + public function getDirectory() + { + return dirname($this->path); + } + + /** + * Returns the absolute file path without dots + * + * @returns string The file path + */ + public function getPath() + { + return $this->path; + } + + /** + * Returns the mime type of the file. + * + * The mime type is guessed using the functions finfo(), mime_content_type() + * and the system binary "file" (in this order), depending on which of those + * is available on the current operating system. + * + * @returns string The guessed mime type, e.g. "application/pdf" + */ + public function getMimeType() + { + $guesser = MimeTypeGuesser::getInstance(); + + return $guesser->guess($this->getPath()); + } + + /** + * Returns the size of this file + * + * @return integer The file size in bytes + */ + public function size() + { + if (false === ($size = filesize($this->getPath()))) + { + throw new FileException(sprintf('Could not read file size of %s', $this->getPath())); + } + + return $size; + } + + /** + * Moves the file to a new location. + * + * @param string $newPath + */ + public function move($newPath) + { + if (!rename($this->getPath(), $newPath)) + { + throw new FileException(sprintf('Could not move file %s to %s', $this->getPath(), $newPath)); + } + } +} \ No newline at end of file diff --git a/src/Symfony/Components/File/MimeType/ContentTypeMimeTypeGuesser.php b/src/Symfony/Components/File/MimeType/ContentTypeMimeTypeGuesser.php new file mode 100644 index 000000000000..4d7644095ff6 --- /dev/null +++ b/src/Symfony/Components/File/MimeType/ContentTypeMimeTypeGuesser.php @@ -0,0 +1,65 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Guesses the mime type using the PHP function mime_content_type(). + * + * @author Bernhard Schussek + */ +class ContentTypeMimeTypeGuesser implements MimeTypeGuesserInterface +{ + /** + * Returns whether this guesser is supported on the corrent OS/PHP setup + * + * @return boolean + */ + static public function isSupported() + { + return function_exists('mime_content_type'); + } + + /** + * Guesses the mime type of the file with the given path + * + * @see MimeTypeGuesserInterface::guess() + */ + public function guess($path) + { + if (!is_file($path)) + { + throw new FileNotFoundException($path); + } + + if (!is_readable($path)) + { + throw new AccessDeniedException($path); + } + + if (!self::isSupported() || !is_readable($path)) + { + return null; + } + + $type = mime_content_type($path); + + // remove charset (added as of PHP 5.3) + if (false !== $pos = strpos($type, ';')) + { + $type = substr($type, 0, $pos); + } + + return $type; + } +} diff --git a/src/Symfony/Components/File/MimeType/FileBinaryMimeTypeGuesser.php b/src/Symfony/Components/File/MimeType/FileBinaryMimeTypeGuesser.php new file mode 100644 index 000000000000..98fc099983b3 --- /dev/null +++ b/src/Symfony/Components/File/MimeType/FileBinaryMimeTypeGuesser.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Guesses the mime type with the binary "file" (only available on *nix) + * + * @author Bernhard Schussek + */ +class FileBinaryMimeTypeGuesser implements MimeTypeGuesserInterface +{ + /** + * Guesses the mime type of the file with the given path + * + * @see MimeTypeGuesserInterface::guess() + */ + public function guess($path) + { + if (!is_file($path)) + { + throw new FileNotFoundException($path); + } + + if (!is_readable($path)) + { + throw new AccessDeniedException($path); + } + + ob_start(); + + // need to use --mime instead of -i. see #6641 + passthru(sprintf('file -b --mime %s 2>/dev/null', escapeshellarg($path)), $return); + if ($return > 0) + { + ob_end_clean(); + + return null; + } + + $type = trim(ob_get_clean()); + + if (!preg_match('#^([a-z0-9\-]+/[a-z0-9\-]+)#i', $type, $match)) + { + // it's not a type, but an error message + return null; + } + + return $match[1]; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/File/MimeType/FileinfoMimeTypeGuesser.php b/src/Symfony/Components/File/MimeType/FileinfoMimeTypeGuesser.php new file mode 100644 index 000000000000..455a38c6ea89 --- /dev/null +++ b/src/Symfony/Components/File/MimeType/FileinfoMimeTypeGuesser.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Guesses the mime type using the PECL extension FileInfo + * + * @author Bernhard Schussek + */ +class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface +{ + /** + * Returns whether this guesser is supported on the corrent OS/PHP setup + * + * @return boolean + */ + static public function isSupported() + { + return function_exists('finfo_open'); + } + + /** + * Guesses the mime type of the file with the given path + * + * @see MimeTypeGuesserInterface::guess() + */ + public function guess($path) + { + if (!is_file($path)) + { + throw new FileNotFoundException($path); + } + + if (!is_readable($path)) + { + throw new AccessDeniedException($path); + } + + if (!self::isSupported()) + { + return null; + } + + if (!$finfo = new \finfo(FILEINFO_MIME)) + { + return null; + } + + $type = $finfo->file($path); + + // remove charset (added as of PHP 5.3) + if (false !== $pos = strpos($type, ';')) + { + $type = substr($type, 0, $pos); + } + + return $type; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/File/MimeType/MimeTypeGuesser.php b/src/Symfony/Components/File/MimeType/MimeTypeGuesser.php new file mode 100644 index 000000000000..31c96348754c --- /dev/null +++ b/src/Symfony/Components/File/MimeType/MimeTypeGuesser.php @@ -0,0 +1,129 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A singleton mime type guesser. + * + * By default, all mime type guessers provided by the framework are installed + * (if available on the current OS/PHP setup). You can register custom + * guessers by calling the register() method on the singleton instance. + * + * + * $guesser = MimeTypeGuesser::getInstance(); + * $guesser->register(new MyCustomMimeTypeGuesser()); + * + * + * The last registered guesser is preferred over previously registered ones. + * + * @author Bernhard Schussek + */ +class MimeTypeGuesser implements MimeTypeGuesserInterface +{ + /** + * The singleton instance + * @var MimeTypeGuesser + */ + static private $instance = null; + + /** + * All registered MimeTypeGuesserInterface instances + * @var array + */ + protected $guessers = array(); + + /** + * Returns the singleton instance + * + * @return MimeTypeGuesser + */ + static public function getInstance() + { + if (is_null(self::$instance)) + { + self::$instance = new self(); + } + + return self::$instance; + } + + /** + * Registers all natively provided mime type guessers + */ + private function __construct() + { + $this->register(new FileBinaryMimeTypeGuesser()); + + if (ContentTypeMimeTypeGuesser::isSupported()) + { + $this->register(new ContentTypeMimeTypeGuesser()); + } + + if (FileinfoMimeTypeGuesser::isSupported()) + { + $this->register(new FileinfoMimeTypeGuesser()); + } + } + + /** + * Registers a new mime type guesser + * + * When guessing, this guesser is preferred over previously registered ones. + * + * @param MimeTypeGuesserInterface $guesser + */ + public function register(MimeTypeGuesserInterface $guesser) + { + array_unshift($this->guessers, $guesser); + } + + /** + * Tries to guess the mime type of the given file + * + * The file is passed to each registered mime type guesser in reverse order + * of their registration (last registered is queried first). Once a guesser + * returns a value that is not NULL, this method terminates and returns the + * value. + * + * @param string $path The path to the file + * @return string The mime type or NULL, if none could be guessed + * @throws FileException If the file does not exist + */ + public function guess($path) + { + if (!is_file($path)) + { + throw new FileNotFoundException($path); + } + + if (!is_readable($path)) + { + throw new AccessDeniedException($path); + } + + $mimeType = null; + + foreach ($this->guessers as $guesser) + { + $mimeType = $guesser->guess($path); + + if (!is_null($mimeType)) + { + break; + } + } + + return $mimeType; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/File/MimeType/MimeTypeGuesserInterface.php b/src/Symfony/Components/File/MimeType/MimeTypeGuesserInterface.php new file mode 100644 index 000000000000..e29ce2fd34c1 --- /dev/null +++ b/src/Symfony/Components/File/MimeType/MimeTypeGuesserInterface.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Guesses the mime type of a file + * + * @author Bernhard Schussek + */ +interface MimeTypeGuesserInterface +{ + /** + * Guesses the mime type of the file with the given path + * + * @param string $path The path to the file + * @return string The mime type or NULL, if none could be guessed + * @throws FileNotFoundException If the file does not exist + * @throws AccessDeniedException If the file could not be read + */ + public function guess($path); +} \ No newline at end of file diff --git a/src/Symfony/Components/File/UploadedFile.php b/src/Symfony/Components/File/UploadedFile.php new file mode 100644 index 000000000000..73524a5fa0d2 --- /dev/null +++ b/src/Symfony/Components/File/UploadedFile.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A file uploaded through a form. + * + * @author Bernhard Schussek + * @author Florian Eckerstorfer + */ +class UploadedFile extends File +{ + protected $originalName; + protected $mimeType; + protected $size; + protected $error; + protected $moved = false; + + /** + * Accepts the information of the uploaded file as provided by the PHP + * global $_FILES. + * + * @param string $tmpName The full temporary path to the file + * @param string $name The original file name + * @param string $type The type of the file as provided by PHP + * @param integer $size The file size + * @param string $error The error constant of the upload. Should be + * one of PHP's UPLOAD_XXX constants. + */ + public function __construct($path, $originalName, $mimeType, $size, $error) + { + if (!ini_get('file_uploads')) + { + throw new FileException(sprintf('Unable to create UploadedFile because "file_uploads" is disabled in your php.ini file (%s)', get_cfg_var('cfg_file_path'))); + } + + parent::__construct($path); + + if (is_null($error)) + { + $error = UPLOAD_ERR_OK; + } + + if (is_null($mimeType)) + { + $mimeType = 'application/octet-stream'; + } + + $this->originalName = (string)$originalName; + $this->mimeType = $mimeType; + $this->size = $size; + $this->error = $error; + } + + /** + * Returns the mime type of the file. + * + * The mime type is guessed using the functions finfo(), mime_content_type() + * and the system binary "file" (in this order), depending on which of those + * is available on the current operating system. + * + * @returns string The guessed mime type, e.g. "application/pdf" + */ + public function getMimeType() + { + $mimeType = parent::getMimeType(); + + if (is_null($mimeType)) + { + $mimeType = $this->mimeType; + } + + return $mimeType; + } + + /** + * Returns the original file name including its extension. + * + * @returns string The file name + */ + public function getOriginalName() + { + return $this->originalName; + } + + /** + * Returns the upload error. + * + * If the upload was successful, the constant UPLOAD_ERR_OK is returned. + * Otherwise one of the other UPLOAD_ERR_XXX constants is returned. + * + * @returns integer The upload error + */ + public function getError() + { + return $this->error; + } + + /** + * Moves the file to a new location. + * + * @param string $newPath + */ + public function move($newPath) + { + if (!$this->moved) + { + if (!move_uploaded_file($this->getPath(), $newPath)) + { + throw new FileException(sprintf('Could not move file %s to %s', $this->getPath(), $newPath)); + } + + $this->moved = true; + } + else + { + parent::move($newPath); + } + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/BirthdayField.php b/src/Symfony/Components/Form/BirthdayField.php new file mode 100644 index 000000000000..34745cc67bbb --- /dev/null +++ b/src/Symfony/Components/Form/BirthdayField.php @@ -0,0 +1,34 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A field for entering a birthday date + * + * This field is a preconfigured DateField with allowed years between the + * current year and 120 years in the past. + * + * @author Bernhard Schussek + */ +class BirthdayField extends DateField +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + $currentYear = date('Y'); + + $this->addOption('years', range($currentYear-120, $currentYear)); + + parent::configure(); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/CheckboxField.php b/src/Symfony/Components/Form/CheckboxField.php new file mode 100644 index 000000000000..1fae41e0d7f0 --- /dev/null +++ b/src/Symfony/Components/Form/CheckboxField.php @@ -0,0 +1,32 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A checkbox field for selecting boolean values. + * + * @author Bernhard Schussek + */ +class CheckboxField extends ToggleField +{ + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + return parent::render(array_merge(array( + 'type' => 'checkbox', + ), $attributes)); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/ChoiceField.php b/src/Symfony/Components/Form/ChoiceField.php new file mode 100644 index 000000000000..91a1352824e2 --- /dev/null +++ b/src/Symfony/Components/Form/ChoiceField.php @@ -0,0 +1,288 @@ + + */ +class ChoiceField extends HybridField +{ + /** + * Stores the preferred choices with the choices as keys + * @var array + */ + protected $preferredChoices = array(); + + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->addRequiredOption('choices'); + $this->addOption('preferred_choices', array()); + $this->addOption('separator', '----------'); + $this->addOption('multiple', false); + $this->addOption('expanded', false); + $this->addOption('empty_value', ''); + $this->addOption('translate_choices', false); + + if (count($this->getOption('preferred_choices')) > 0) + { + $this->preferredChoices = array_flip($this->getOption('preferred_choices')); + + if (false && $diff = array_diff_key($this->options, $this->knownOptions)) + { + //throw new InvalidOptionsException(sprintf('%s does not support the following options: "%s".', get_class($this), implode('", "', array_keys($diff))), array_keys($diff)); + } + } + + if ($this->getOption('expanded')) + { + $this->setFieldMode(self::GROUP); + + $choices = $this->getOption('choices'); + + foreach ($this->getOption('preferred_choices') as $choice) + { + $this->add($this->newChoiceField($choice, $choices[$choice])); + unset($choices[$choice]); + } + + foreach ($this->getOption('choices') as $choice => $value) + { + $this->add($this->newChoiceField($choice, $value)); + } + } + else + { + $this->setFieldMode(self::FIELD); + } + } + + /** + * Returns a new field of type radio button or checkbox. + * + * @param string $key The key for the option + * @param string $label The label for the option + */ + protected function newChoiceField($choice, $label) + { + if ($this->getOption('multiple')) + { + return new CheckboxField($choice, array( + 'value' => $choice, + 'label' => $label, + 'translate_label' => $this->getOption('translate_choices'), + )); + } + else + { + return new RadioField($choice, array( + 'value' => $choice, + 'label' => $label, + 'translate_label' => $this->getOption('translate_choices'), + )); + } + } + + /** + * {@inheritDoc} + * + * Takes care of converting the input from a single radio button + * to an array. + */ + public function bind($value) + { + if (!$this->getOption('multiple') && $this->getOption('expanded')) + { + $value = $value === null ? array() : array($value => true); + } + + parent::bind($value); + } + + /** + * Transforms a single choice or an array of choices to a format appropriate + * for the nested checkboxes/radio buttons. + * + * The result is an array with the options as keys and true/false as values, + * depending on whether a given option is selected. If this field is rendered + * as select tag, the value is not modified. + * + * @param mixed $value An array if "multiple" is set to true, a scalar + * value otherwise. + * @return mixed An array if "expanded" or "multiple" is set to true, + * a scalar value otherwise. + */ + protected function transform($value) + { + if ($this->getOption('expanded')) + { + $choices = $this->getOption('choices'); + + foreach ($choices as $choice => $_) + { + $choices[$choice] = $this->getOption('multiple') + ? in_array($choice, (array)$value, true) + : ($choice === $value); + } + + return $choices; + } + else + { + return parent::transform($value); + } + } + + /** + * Transforms a checkbox/radio button array to a single choice or an array + * of choices. + * + * The input value is an array with the choices as keys and true/false as + * values, depending on whether a given choice is selected. The output + * is an array with the selected choices or a single selected choice. + * + * @param mixed $value An array if "expanded" or "multiple" is set to true, + * a scalar value otherwise. + * @return mixed $value An array if "multiple" is set to true, a scalar + * value otherwise. + */ + protected function reverseTransform($value) + { + if ($this->getOption('expanded')) + { + $choices = array(); + + foreach ($value as $choice => $selected) + { + if ($selected) + { + $choices[] = $choice; + } + } + + if ($this->getOption('multiple')) + { + return $choices; + } + else + { + return count($choices) > 0 ? current($choices) : null; + } + } + else + { + return parent::reverseTransform($value); + } + } + + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + if ($this->getOption('expanded')) + { + $html = ""; + + foreach ($this as $field) + { + $html .= $field->render()."\n"; + } + + return $html; + } + else + { + $attrs['id'] = $this->getId(); + $attrs['name'] = $this->getName(); + $attrs['disabled'] = $this->isDisabled(); + + // Add "[]" to the name in case a select tag with multiple options is + // displayed. Otherwise only one of the selected options is sent in the + // POST request. + if ($this->getOption('multiple') && !$this->getOption('expanded')) + { + $attrs['name'] .= '[]'; + } + + if ($this->getOption('multiple')) + { + $attrs['multiple'] = 'multiple'; + } + + $selected = array_flip(array_map('strval', (array)$this->getDisplayedData())); + $html = "\n"; + + if (!$this->isRequired()) + { + $html .= $this->renderChoices(array('' => $this->getOption('empty_value')), $selected)."\n"; + } + + $choices = $this->getOption('choices'); + + if (count($this->preferredChoices) > 0) + { + $html .= $this->renderChoices(array_intersect_key($choices, $this->preferredChoices), $selected)."\n"; + $html .= $this->generator->contentTag('option', $this->getOption('separator'), array('disabled' => true))."\n"; + } + + $html .= $this->renderChoices(array_diff_key($choices, $this->preferredChoices), $selected)."\n"; + + return $this->generator->contentTag('select', $html, array_merge($attrs, $attributes)); + } + } + + /** + * Returns an array of option tags for the choice field + * + * @return array An array of option tags + */ + protected function renderChoices(array $choices, array $selected) + { + $options = array(); + + foreach ($choices as $key => $option) + { + if (is_array($option)) + { + $options[] = $this->generator->contentTag( + 'optgroup', + "\n".$this->renderChoices($option, $selected)."\n", + array('label' => $this->generator->escape($key)) + ); + } + else + { + $attributes = array('value' => $this->generator->escape($key)); + + if (isset($selected[strval($key)])) + { + $attributes['selected'] = true; + } + + if ($this->getOption('translate_choices')) + { + $option = $this->translate($option); + } + + $options[] = $this->generator->contentTag( + 'option', + $this->generator->escape($option), + $attributes + ); + } + } + + return implode("\n", $options); + } +} diff --git a/src/Symfony/Components/Form/CollectionField.php b/src/Symfony/Components/Form/CollectionField.php new file mode 100644 index 000000000000..76b7f71917a7 --- /dev/null +++ b/src/Symfony/Components/Form/CollectionField.php @@ -0,0 +1,103 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * @package symfony + * @subpackage form + * @author Bernhard Schussek + * @version SVN: $Id: FieldGroup.php 79 2009-12-08 12:53:15Z bernhard $ + */ +class CollectionField extends FieldGroup +{ + /** + * The prototype for the inner fields + * @var FieldInterface + */ + protected $prototype; + + /** + * Repeats the given field twice to verify the user's input + * + * @param FieldInterface $innerField + */ + public function __construct(FieldInterface $innerField, array $options = array()) + { + $this->prototype = $innerField; + + parent::__construct($innerField->getKey(), $options); + } + + protected function configure() + { + $this->addOption('modifiable', false); + + if ($this->getOption('modifiable')) + { + $field = $this->newField('$$key$$', null); + // TESTME + $field->setRequired(false); + $this->add($field); + } + } + + public function setData($collection) + { + if (!is_array($collection) && !$collection instanceof Traversable) + { + throw new UnexpectedTypeException('The data must be an array'); + } + + foreach ($collection as $name => $value) + { + $this->add($this->newField($name, $name)); + } + + parent::setData($collection); + } + + public function bind($taintedData) + { + if (is_null($taintedData)) + { + $taintedData = array(); + } + + foreach ($this as $name => $field) + { + if (!isset($taintedData[$name]) && $this->getOption('modifiable') && $name != '$$key$$') + { + $this->remove($name); + } + } + + foreach ($taintedData as $name => $value) + { + if (!isset($this[$name]) && $this->getOption('modifiable')) + { + $this->add($this->newField($name, $name)); + } + } + + return parent::bind($taintedData); + } + + protected function newField($key, $propertyPath) + { + $field = clone $this->prototype; + $field->setKey($key); + $field->setPropertyPath($propertyPath === null ? null : '['.$propertyPath.']'); + return $field; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/Configurable.php b/src/Symfony/Components/Form/Configurable.php new file mode 100644 index 000000000000..bba553c3c15b --- /dev/null +++ b/src/Symfony/Components/Form/Configurable.php @@ -0,0 +1,142 @@ + + */ +abstract class Configurable +{ + /** + * The options and their values + * @var array + */ + private $options = array(); + + /** + * The names of the valid options + * @var array + */ + private $knownOptions = array(); + + /** + * The names of the required options + * @var array + */ + private $requiredOptions = array(); + + /** + * The allowed values for each option + * @var array + */ + private $allowedValues = array(); + + /** + * Reads, validates and stores the given options + * + * @param array $options + */ + public function __construct(array $options = array()) + { + $this->options = array_merge($this->options, $options); + + $this->configure(); + + // check option names + if ($diff = array_diff_key($this->options, $this->knownOptions)) + { + throw new InvalidOptionsException(sprintf('%s does not support the following options: "%s".', get_class($this), implode('", "', array_keys($diff))), array_keys($diff)); + } + + // check required options + if ($diff = array_diff_key($this->requiredOptions, $this->options)) + { + throw new MissingOptionsException(sprintf('%s requires the following options: \'%s\'.', get_class($this), implode('", "', array_keys($diff))), array_keys($diff)); + } + } + + /** + * Configures the valid options + * + * This method should call addOption() or addRequiredOption() for every + * accepted option. + */ + protected function configure() + { + } + + /** + * Returns an option value. + * + * @param string $name The option name + * + * @return mixed The option value + */ + public function getOption($name) + { + return array_key_exists($name, $this->options) ? $this->options[$name] : null; + } + + /** + * Adds a new option value with a default value. + * + * @param string $name The option name + * @param mixed $value The default value + */ + protected function addOption($name, $value = null, array $allowedValues = array()) + { + $this->knownOptions[$name] = true; + + if (!array_key_exists($name, $this->options)) + { + $this->options[$name] = $value; + } + + if (count($allowedValues) > 0 && !in_array($this->options[$name], $allowedValues)) + { + throw new InvalidOptionsException(sprintf('The option "%s" is expected to be one of "%s", but is "%s"', $name, implode('", "', $allowedValues), $this->options[$name]), array($name)); + } + } + + /** + * Adds a required option. + * + * @param string $name The option name + */ + protected function addRequiredOption($name, array $allowedValues = array()) + { + $this->knownOptions[$name] = true; + $this->requiredOptions[$name] = true; + + // only test if the option is set, otherwise an error will be thrown + // anyway + if (isset($this->options[$name]) && count($allowedValues) > 0 && !in_array($this->options[$name], $allowedValues)) + { + throw new InvalidOptionsException(sprintf('The option "%s" is expected to be one of "%s", but is "%s"', $name, implode('", "', $allowedValues), $this->options[$name]), array($name)); + } + } + + /** + * Returns true if the option exists. + * + * @param string $name The option name + * + * @return bool true if the option is set, false otherwise + */ + public function hasOption($name) + { + return isset($this->options[$name]); + } +} diff --git a/src/Symfony/Components/Form/Configurator/ConfiguratorInterface.php b/src/Symfony/Components/Form/Configurator/ConfiguratorInterface.php new file mode 100644 index 000000000000..88c8f12943f6 --- /dev/null +++ b/src/Symfony/Components/Form/Configurator/ConfiguratorInterface.php @@ -0,0 +1,14 @@ +metaData = $metaData; + } + + public function initialize($object) + { + $this->classMetaData = $this->metaData->getClassMetaData(get_class($object)); + } + + public function getClass($fieldName) + { + + } + + public function getOptions($fieldName) + { + + } + + public function isRequired($fieldName) + { + return $this->classMetaData->getPropertyMetaData($fieldName)->hasConstraint('NotNull') + || $this->classMetaData->getPropertyMetaData($fieldName)->hasConstraint('NotEmpty'); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/DateField.php b/src/Symfony/Components/Form/DateField.php new file mode 100644 index 000000000000..a0122ea0a841 --- /dev/null +++ b/src/Symfony/Components/Form/DateField.php @@ -0,0 +1,262 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +class DateField extends HybridField +{ + const FULL = 'full'; + const LONG = 'long'; + const MEDIUM = 'medium'; + const SHORT = 'short'; + + const DATETIME = 'datetime'; + const STRING = 'string'; + const TIMESTAMP = 'timestamp'; + const RAW = 'raw'; + + const INPUT = 'input'; + const CHOICE = 'choice'; + + protected static $formats = array( + self::FULL, + self::LONG, + self::MEDIUM, + self::SHORT, + ); + + protected static $intlFormats = array( + self::FULL => \IntlDateFormatter::FULL, + self::LONG => \IntlDateFormatter::LONG, + self::MEDIUM => \IntlDateFormatter::MEDIUM, + self::SHORT => \IntlDateFormatter::SHORT, + ); + + protected static $widgets = array( + self::INPUT, + self::CHOICE, + ); + + protected static $types = array( + self::DATETIME, + self::STRING, + self::TIMESTAMP, + self::RAW, + ); + + /** + * The ICU formatter instance + * @var \IntlDateFormatter + */ + protected $formatter; + + /** + * Configures the text field. + * + * Available options: + * + * * widget: How to render the field ("input" or "select"). Default: "input" + * * years: An array of years for the year select tag (optional) + * * months: An array of months for the month select tag (optional) + * * days: An array of days for the day select tag (optional) + * * format: See DateValueTransformer. Default: medium + * * type: The type of the date ("date", "datetime" or "timestamp"). Default: "date" + * * data_timezone: The timezone of the data + * * user_timezone: The timezone of the user entering a new value + * * pattern: The pattern for the select boxes when "widget" is "select". + * You can use the placeholders "%year%", "%month%" and "%day%". + * Default: locale dependent + * + * @param array $options Options for this field + * @throws \InvalidArgumentException Thrown if you want to show a timestamp with the select widget. + */ + protected function configure() + { + $this->addOption('years', range(date('Y') - 5, date('Y') + 5)); + $this->addOption('months', range(1, 12)); + $this->addOption('days', range(1, 31)); + $this->addOption('format', self::MEDIUM, self::$formats); + $this->addOption('type', self::DATETIME, self::$types); + $this->addOption('data_timezone', 'UTC'); + $this->addOption('user_timezone', 'UTC'); + $this->addOption('widget', self::CHOICE, self::$widgets); + $this->addOption('pattern'); + + $this->formatter = new \IntlDateFormatter( + $this->locale, + self::$intlFormats[$this->getOption('format')], + \IntlDateFormatter::NONE + ); + + $transformers = array(); + + if ($this->getOption('type') === self::STRING) + { + $transformers[] = new StringToDateTimeTransformer(array( + 'input_timezone' => $this->getOption('data_timezone'), + 'output_timezone' => $this->getOption('data_timezone'), + 'format' => 'Y-m-d', + )); + } + else if ($this->getOption('type') === self::TIMESTAMP) + { + $transformers[] = new TimestampToDateTimeTransformer(array( + 'output_timezone' => $this->getOption('data_timezone'), + 'input_timezone' => $this->getOption('data_timezone'), + )); + } + else if ($this->getOption('type') === self::RAW) + { + $transformers[] = new ReversedTransformer(new DateTimeToArrayTransformer(array( + 'input_timezone' => $this->getOption('data_timezone'), + 'output_timezone' => $this->getOption('data_timezone'), + 'fields' => array('year', 'month', 'day'), + ))); + } + + if ($this->getOption('widget') === self::INPUT) + { + $transformers[] = new DateTimeToLocalizedStringTransformer(array( + 'date_format' => $this->getOption('format'), + 'time_format' => DateTimeToLocalizedStringTransformer::NONE, + 'input_timezone' => $this->getOption('data_timezone'), + 'output_timezone' => $this->getOption('user_timezone'), + )); + + $this->setFieldMode(self::FIELD); + } + else + { + $transformers[] = new DateTimeToArrayTransformer(array( + 'input_timezone' => $this->getOption('data_timezone'), + 'output_timezone' => $this->getOption('user_timezone'), + )); + + $this->setFieldMode(self::GROUP); + + $this->add(new ChoiceField('year', array( + 'choices' => $this->generatePaddedChoices($this->getOption('years'), 4), + ))); + $this->add(new ChoiceField('month', array( + 'choices' => $this->generateMonthChoices($this->getOption('months')), + ))); + $this->add(new ChoiceField('day', array( + 'choices' => $this->generatePaddedChoices($this->getOption('days'), 2), + ))); + } + + if (count($transformers) > 0) + { + $this->setValueTransformer(new ValueTransformerChain($transformers)); + } + } + + /** + * Generates an array of choices for the given values + * + * If the values are shorter than $padLength characters, they are padded with + * zeros on the left side. + * + * @param array $values The available choices + * @param integer $padLength The length to pad the choices + * @return array An array with the input values as keys and the + * padded values as values + */ + protected function generatePaddedChoices(array $values, $padLength) + { + $choices = array(); + + foreach ($values as $value) + { + $choices[$value] = str_pad($value, $padLength, '0', STR_PAD_LEFT); + } + + return $choices; + } + + /** + * Generates an array of localized month choices + * + * @param array $months The month numbers to generate + * @return array The localized months respecting the configured + * locale and date format + */ + protected function generateMonthChoices(array $months) + { + $pattern = $this->formatter->getPattern(); + + if (preg_match('/M+/', $pattern, $matches)) + { + $this->formatter->setPattern($matches[0]); + $choices = array(); + + foreach ($months as $month) + { + $choices[$month] = $this->formatter->format(gmmktime(0, 0, 0, $month)); + } + + $this->formatter->setPattern($pattern); + } + else + { + $choices = $this->generatePaddedChoices($months, 2); + } + + return $choices; + } + + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + if ($this->getOption('widget') === self::INPUT) + { + return $this->generator->tag('input', array_merge(array( + 'id' => $this->getId(), + 'name' => $this->getName(), + 'value' => $this->getDisplayedData(), + 'type' => 'text', + ), $attributes)); + } + else + { + // set order as specified in the pattern + if ($this->getOption('pattern')) + { + $pattern = $this->getOption('pattern'); + } + // set right order with respect to locale (e.g.: de_DE=dd.MM.yy; en_US=M/d/yy) + // lookup various formats at http://userguide.icu-project.org/formatparse/datetime + else if (preg_match('/^([yMd]+).+([yMd]+).+([yMd]+)$/', $this->formatter->getPattern())) + { + $pattern = preg_replace(array('/y+/', '/M+/', '/d+/'), array('%year%', '%month%', '%day%'), $this->formatter->getPattern()); + } + // default fallback + else + { + $pattern = '%year%-%month%-%day%'; + } + + return str_replace(array('%year%', '%month%', '%day%'), array( + $this->get('year')->render($attributes), + $this->get('month')->render($attributes), + $this->get('day')->render($attributes), + ), $pattern); + } + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/DateTimeField.php b/src/Symfony/Components/Form/DateTimeField.php new file mode 100644 index 000000000000..675b8b9da8e9 --- /dev/null +++ b/src/Symfony/Components/Form/DateTimeField.php @@ -0,0 +1,128 @@ + + */ +class DateTimeField extends FieldGroup +{ + const DATETIME = 'datetime'; + const STRING = 'string'; + const TIMESTAMP = 'timestamp'; + + protected static $types = array( + self::DATETIME, + self::STRING, + self::TIMESTAMP, + ); + + protected static $dateWidgets = array( + DateField::CHOICE, + DateField::INPUT, + ); + + protected static $timeWidgets = array( + TimeField::CHOICE, + TimeField::INPUT, + ); + + /** + * {@inheritDoc} + */ + public function configure() + { + $this->addOption('years', range(date('Y') - 5, date('Y') + 5)); + $this->addOption('months', range(1, 12)); + $this->addOption('days', range(1, 31)); + $this->addOption('hours', range(0, 23)); + $this->addOption('minutes', range(0, 59)); + $this->addOption('seconds', range(0, 59)); + $this->addOption('data_timezone', 'UTC'); + $this->addOption('user_timezone', 'UTC'); + $this->addOption('date_widget', DateField::INPUT, self::$dateWidgets); + $this->addOption('time_widget', TimeField::CHOICE, self::$timeWidgets); + $this->addOption('type', self::DATETIME, self::$types); + $this->addOption('with_seconds', false); + + $this->add(new DateField('date', array( + 'type' => DateField::RAW, + 'widget' => $this->getOption('date_widget'), + 'data_timezone' => $this->getOption('user_timezone'), + 'user_timezone' => $this->getOption('user_timezone'), + 'years' => $this->getOption('years'), + 'months' => $this->getOption('months'), + 'days' => $this->getOption('days'), + ))); + $this->add(new TimeField('time', array( + 'type' => TimeField::RAW, + 'widget' => $this->getOption('time_widget'), + 'data_timezone' => $this->getOption('user_timezone'), + 'user_timezone' => $this->getOption('user_timezone'), + 'with_seconds' => $this->getOption('with_seconds'), + 'hours' => $this->getOption('hours'), + 'minutes' => $this->getOption('minutes'), + 'seconds' => $this->getOption('seconds'), + ))); + + $transformers = array(); + + if ($this->getOption('type') == self::STRING) + { + $transformers[] = new StringToDateTimeTransformer(array( + 'input_timezone' => $this->getOption('data_timezone'), + 'output_timezone' => $this->getOption('data_timezone'), + )); + } + else if ($this->getOption('type') == self::TIMESTAMP) + { + $transformers[] = new TimestampToDateTimeTransformer(array( + 'input_timezone' => $this->getOption('data_timezone'), + 'output_timezone' => $this->getOption('data_timezone'), + )); + } + + $transformers[] = new DateTimeToArrayTransformer(array( + 'input_timezone' => $this->getOption('data_timezone'), + 'output_timezone' => $this->getOption('user_timezone'), + )); + + $this->setValueTransformer(new ValueTransformerChain($transformers)); + } + + /** + * {@inheritDoc} + */ + protected function transform($value) + { + $value = parent::transform($value); + + return array('date' => $value, 'time' => $value); + } + + /** + * {@inheritDoc} + */ + protected function reverseTransform($value) + { + return parent::reverseTransform(array_merge($value['date'], $value['time'])); + } + + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + $html = $this->get('date')->render($attributes)."\n"; + $html .= $this->get('time')->render($attributes); + + return $html; + } +} diff --git a/src/Symfony/Components/Form/Exception/AlreadyBoundException.php b/src/Symfony/Components/Form/Exception/AlreadyBoundException.php new file mode 100644 index 000000000000..9b87a1ea5a16 --- /dev/null +++ b/src/Symfony/Components/Form/Exception/AlreadyBoundException.php @@ -0,0 +1,7 @@ +options = $options; + } + + public function getOptions() + { + return $this->options; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/Exception/InvalidPropertyException.php b/src/Symfony/Components/Form/Exception/InvalidPropertyException.php new file mode 100644 index 000000000000..d8a5d92333e7 --- /dev/null +++ b/src/Symfony/Components/Form/Exception/InvalidPropertyException.php @@ -0,0 +1,7 @@ +options = $options; + } + + public function getOptions() + { + return $this->options; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/Exception/NotInitializedException.php b/src/Symfony/Components/Form/Exception/NotInitializedException.php new file mode 100644 index 000000000000..e0e4ffffb9fc --- /dev/null +++ b/src/Symfony/Components/Form/Exception/NotInitializedException.php @@ -0,0 +1,7 @@ +addOption('trim', true); + $this->addOption('required', true); + $this->addOption('disabled', false); + $this->addOption('property_path', (string)$key); + + $this->key = (string)$key; + $this->locale = \Locale::getDefault(); + $this->generator = new HtmlGenerator(); + + parent::__construct($options); + + $this->transformedData = $this->transform($this->data); + $this->required = $this->getOption('required'); + + $this->setPropertyPath($this->getOption('property_path')); + } + + /** + * Clones this field. + */ + public function __clone() + { + // TODO + } + + /** + * Returns the data of the field as it is displayed to the user. + * + * @return string|array When the field is not bound, the transformed + * default data is returned. When the field is bound, + * the bound data is returned. + */ + public function getDisplayedData() + { + return $this->getTransformedData(); + } + + /** + * Returns the data transformed by the value transformer + * + * @return string + */ + protected function getTransformedData() + { + return $this->transformedData; + } + + /** + * {@inheritDoc} + */ + public function setPropertyPath($propertyPath) + { + $this->propertyPath = empty($propertyPath) ? null : new PropertyPath($propertyPath); + } + + /** + * {@inheritDoc} + */ + public function getPropertyPath() + { + return $this->propertyPath; + } + + /** + * {@inheritDoc} + */ + public function setKey($key) + { + $this->key = (string)$key; + } + + /** + * {@inheritDoc} + */ + public function getKey() + { + return $this->key; + } + + /** + * {@inheritDoc} + */ + public function getName() + { + return is_null($this->parent) ? $this->key : $this->parent->getName().'['.$this->key.']'; + } + + /** + * {@inheritDoc} + */ + public function getId() + { + return is_null($this->parent) ? $this->key : $this->parent->getId().'_'.$this->key; + } + + /** + * {@inheritDoc} + */ + public function setRequired($required) + { + $this->required = $required; + } + + /** + * {@inheritDoc} + */ + public function isRequired() + { + if (is_null($this->parent) || $this->parent->isRequired()) + { + return $this->required; + } + else + { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function isDisabled() + { + if (is_null($this->parent) || !$this->parent->isDisabled()) + { + return $this->getOption('disabled'); + } + else + { + return true; + } + } + + /** + * {@inheritDoc} + */ + public function setGenerator(HtmlGeneratorInterface $generator) + { + $this->generator = $generator; + } + + /** + * {@inheritDoc} + */ + public function isMultipart() + { + return false; + } + + /** + * Returns true if the widget is hidden. + * + * @return Boolean true if the widget is hidden, false otherwise + */ + public function isHidden() + { + return false; + } + + /** + * {@inheritDoc} + */ + public function setParent(FieldInterface $parent = null) + { + $this->parent = $parent; + } + + /** + * Returns the parent field. + * + * @return FieldInterface The parent field + */ + public function getParent() + { + return $this->parent; + } + + /** + * Updates the field with default data + * + * @see FieldInterface + */ + public function setData($data) + { + $this->data = $data; + $this->transformedData = $this->transform($data); + } + + /** + * Binds POST data to the field, transforms and validates it. + * + * @param string|array $taintedData The POST data + * @return boolean Whether the form is valid + * @throws AlreadyBoundException when the field is already bound + */ + public function bind($taintedData) + { + $this->transformedData = is_array($taintedData) || is_object($taintedData) ? $taintedData : (string)$taintedData; + $this->bound = true; + $this->errors = array(); + + if (is_string($this->transformedData) && $this->getOption('trim')) + { + $this->transformedData = trim($this->transformedData); + } + + try + { + $this->data = $this->processData($data = $this->reverseTransform($this->transformedData)); + $this->transformedData = $this->transform($this->data); + } + catch (TransformationFailedException $e) + { + // TODO better text + // TESTME + $this->addError('invalid (localized)'); + } + } + + /** + * Processes the bound reverse-transformed data. + * + * This method can be overridden if you want to modify the data entered + * by the user. Note that the data is already in reverse transformed format. + * + * This method will not be called if reverse transformation fails. + * + * @param mixed $data + * @return mixed + */ + protected function processData($data) + { + return $data; + } + + /** + * Returns the normalized data of the field. + * + * @return mixed When the field is not bound, the default data is returned. + * When the field is bound, the normalized bound data is + * returned if the field is valid, null otherwise. + */ + public function getData() + { + return $this->data; + } + + /** + * Adds an error to the field. + * + * @see FieldInterface + */ + public function addError($message, PropertyPath $path = null, $type = null) + { + $this->errors[] = $message; + } + + /** + * Returns whether the field is bound. + * + * @return boolean true if the form is bound to input values, false otherwise + */ + public function isBound() + { + return $this->bound; + } + + /** + * Returns whether the field is valid. + * + * @return boolean + */ + public function isValid() + { + return $this->isBound() ? count($this->errors)==0 : false; // TESTME + } + + /** + * Returns weather there are errors. + * + * @return boolean true if form is bound and not valid + */ + public function hasErrors() + { + return $this->isBound() && !$this->isValid(); + } + + /** + * Returns all errors + * + * @return array An array of errors that occured during binding + */ + public function getErrors() + { + return $this->errors; + } + + /** + * Sets the locale of this field. + * + * @see Localizable + */ + public function setLocale($locale) + { + $this->locale = $locale; + + if ($this->valueTransformer !== null && $this->valueTransformer instanceof Localizable) + { + $this->valueTransformer->setLocale($locale); + } + } + + /** + * Sets the translator of this field. + * + * @see Translatable + */ + public function setTranslator(TranslatorInterface $translator) + { + $this->translator = $translator; + + if ($this->valueTransformer !== null && $this->valueTransformer instanceof Translatable) + { + $this->valueTransformer->setTranslator($translator); + } + } + + /** + * Translates the text using the associated translator, if available + * + * If no translator is available, the original text is returned without + * modification. + * + * @param string $text The text to translate + * @param array $parameters The parameters to insert in the text + * @return string The translated text + */ + protected function translate($text, array $parameters = array()) + { + if ($this->translator !== null) + { + $text = $this->translator->translate($text, $parameters); + } + + return $text; + } + + /** + * Injects the locale and the translator into the given object, if set. + * + * The locale is injected only if the object implements Localizable. The + * translator is injected only if the object implements Translatable. + * + * @param object $object + */ + protected function injectLocaleAndTranslator($object) + { + if ($object instanceof Localizable) + { + $object->setLocale($this->locale); + } + + if (!is_null($this->translator) && $object instanceof Translatable) + { + $object->setTranslator($this->translator); + } + } + + /** + * Sets the ValueTransformer. + * + * @param ValueTransformerInterface $valueTransformer + */ + public function setValueTransformer(ValueTransformerInterface $valueTransformer) + { + $this->injectLocaleAndTranslator($valueTransformer); + + $this->valueTransformer = $valueTransformer; + } + + /** + * Returns the ValueTransformer. + * + * @return ValueTransformerInterface + */ + public function getValueTransformer() + { + return $this->valueTransformer; + } + + /** + * Transforms the value if a value transformer is set. + * + * @param mixed $value The value to transform + * @return string + */ + protected function transform($value) + { + if ($value === null) + { + return ''; + } + else if (null === $this->valueTransformer) + { + return $value; + } + else + { + return $this->valueTransformer->transform($value); + } + } + + /** + * Reverse transforms a value if a value transformer is set. + * + * @param string $value The value to reverse transform + * @return mixed + */ + protected function reverseTransform($value) + { + if ($value === '') + { + return null; + } + else if (null === $this->valueTransformer) + { + return $value; + } + else + { + return $this->valueTransformer->reverseTransform($value); + } + } + + /** + * {@inheritDoc} + */ + public function updateFromObject(&$objectOrArray) + { + // TODO throw exception if not object or array + + if ($this->propertyPath !== null) + { + $this->propertyPath->rewind(); + $this->setData($this->readPropertyPath($objectOrArray, $this->propertyPath)); + } + else + { + // pass object through if the property path is empty + $this->setData($objectOrArray); + } + } + + /** + * {@inheritDoc} + */ + public function updateObject(&$objectOrArray) + { + // TODO throw exception if not object or array + + if ($this->propertyPath !== null) + { + $this->propertyPath->rewind(); + $this->updatePropertyPath($objectOrArray, $this->propertyPath); + } + } + + /** + * Recursively reads the value of the property path in the data + * + * @param array|object $objectOrArray An object or array + * @param PropertyPath $propertyPath A property path pointing to a property + * in the object/array. + */ + protected function readPropertyPath(&$objectOrArray, PropertyPath $propertyPath) + { + if (is_object($objectOrArray)) + { + $value = $this->readProperty($objectOrArray, $propertyPath); + } + // arrays need to be treated separately (due to PHP bug?) + // http://bugs.php.net/bug.php?id=52133 + else + { + if (!array_key_exists($propertyPath->getCurrent(), $objectOrArray)) + { + $objectOrArray[$propertyPath->getCurrent()] = array(); + } + + $value =& $objectOrArray[$propertyPath->getCurrent()]; + } + + if ($propertyPath->hasNext()) + { + $propertyPath->next(); + + return $this->readPropertyPath($value, $propertyPath); + } + else + { + return $value; + } + } + + protected function updatePropertyPath(&$objectOrArray, PropertyPath $propertyPath) + { + if ($propertyPath->hasNext()) + { + if (is_object($objectOrArray)) + { + $value = $this->readProperty($objectOrArray, $propertyPath); + } + // arrays need to be treated separately (due to PHP bug?) + // http://bugs.php.net/bug.php?id=52133 + else + { + if (!array_key_exists($propertyPath->getCurrent(), $objectOrArray)) + { + $objectOrArray[$propertyPath->getCurrent()] = array(); + } + + $value =& $objectOrArray[$propertyPath->getCurrent()]; + } + + $propertyPath->next(); + + $this->updatePropertyPath($value, $propertyPath); + } + else + { + $this->updateProperty($objectOrArray, $propertyPath); + } + } + + /** + * Reads a specific element of the given data + * + * If the data is an array, the value at index $element is returned. + * + * If the data is an object, either the result of get{$element}(), + * is{$element}() or the property $element is returned. If none of these + * is publicly available, an exception is thrown + * + * @param object $object The data to read + * @param string $element The element to read from the data + * @return mixed The value of the element + */ + protected function readProperty($object, PropertyPath $propertyPath) + { + if ($propertyPath->isIndex()) + { + if (!$object instanceof \ArrayAccess) + { + throw new InvalidPropertyException(sprintf('Index "%s" cannot be read from object of type "%s" because it doesn\'t implement \ArrayAccess', $propertyPath->getCurrent(), get_class($object))); + } + + return $object[$propertyPath->getCurrent()]; + } + else + { + $reflClass = new \ReflectionClass($object); + $getter = 'get'.ucfirst($propertyPath->getCurrent()); + $isser = 'is'.ucfirst($propertyPath->getCurrent()); + $property = $propertyPath->getCurrent(); + + if ($reflClass->hasMethod($getter)) + { + if (!$reflClass->getMethod($getter)->isPublic()) + { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $getter, $reflClass->getName())); + } + + return $object->$getter(); + } + else if ($reflClass->hasMethod($isser)) + { + if (!$reflClass->getMethod($isser)->isPublic()) + { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $isser, $reflClass->getName())); + } + + return $object->$isser(); + } + else if ($reflClass->hasProperty($property)) + { + if (!$reflClass->getProperty($property)->isPublic()) + { + throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "get%s()" or "is%s()"?', $property, $reflClass->getName(), ucfirst($property), ucfirst($property))); + } + + return $object->$property; + } + else + { + throw new InvalidPropertyException(sprintf('Neither property "%s" nor method "%s()" nor method "%s()" exists in class "%s"', $property, $getter, $isser, $reflClass->getName())); + } + } + } + + protected function updateProperty(&$objectOrArray, PropertyPath $propertyPath) + { + if (is_object($objectOrArray) && $propertyPath->isIndex()) + { + if (!$objectOrArray instanceof \ArrayAccess) + { + throw new InvalidPropertyException(sprintf('Index "%s" cannot be modified in object of type "%s" because it doesn\'t implement \ArrayAccess', $propertyPath->getCurrent(), get_class($objectOrArray))); + } + + $objectOrArray[$propertyPath->getCurrent()] = $this->getData(); + } + else if (is_object($objectOrArray)) + { + $reflClass = new \ReflectionClass($objectOrArray); + $setter = 'set'.ucfirst($propertyPath->getCurrent()); + $property = $propertyPath->getCurrent(); + + if ($reflClass->hasMethod($setter)) + { + if (!$reflClass->getMethod($setter)->isPublic()) + { + throw new PropertyAccessDeniedException(sprintf('Method "%s()" is not public in class "%s"', $setter, $reflClass->getName())); + } + + $objectOrArray->$setter($this->getData()); + } + else if ($reflClass->hasProperty($property)) + { + if (!$reflClass->getProperty($property)->isPublic()) + { + throw new PropertyAccessDeniedException(sprintf('Property "%s" is not public in class "%s". Maybe you should create the method "set%s()"?', $property, $reflClass->getName(), ucfirst($property))); + } + + $objectOrArray->$property = $this->getData(); + } + else + { + throw new InvalidPropertyException(sprintf('Neither element "%s" nor method "%s()" exists in class "%s"', $property, $setter, $reflClass->getName())); + } + } + else + { + $objectOrArray[$propertyPath->getCurrent()] = $this->getData(); + } + } + + /** + * {@inheritDoc} + */ + public function renderErrors() + { + $html = ''; + + if ($this->hasErrors()) + { + $html .= "
    \n"; + + foreach ($this->getErrors() as $error) + { + $html .= "
  • " . $error . "
  • \n"; + } + + $html .= "
\n"; + } + + return $html; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/FieldGroup.php b/src/Symfony/Components/Form/FieldGroup.php new file mode 100644 index 000000000000..eefa3689579a --- /dev/null +++ b/src/Symfony/Components/Form/FieldGroup.php @@ -0,0 +1,637 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * FieldGroup represents an array of widgets bind to names and values. + * + * @package symfony + * @subpackage form + * @author Fabien Potencier + * @version SVN: $Id: FieldGroup.php 247 2010-02-01 09:24:55Z bernhard $ + */ +class FieldGroup extends Field implements \IteratorAggregate, FieldGroupInterface +{ + /** + * Contains all the fields of this group + * @var array + */ + protected $fields = array(); + + /** + * Contains the names of bound values who don't belong to any fields + * @var array + */ + protected $extraFields = array(); + + /** + * Constructor + * + * @see FieldInterface::__construct() + */ + public function __construct($key, array $options = array()) + { + // set the default renderer before calling the configure() method + $this->setRenderer(new TableRenderer()); + + parent::__construct($key, $options); + } + + /** + * Clones this group + */ + public function __clone() + { + foreach ($this->fields as $name => $field) + { + $this->fields[$name] = clone $field; + } + } + + /** + * Adds a new field to this group. A field must have a unique name within + * the group. Otherwise the existing field is overwritten. + * + * If you add a nested group, this group should also be represented in the + * object hierarchy. If you want to add a group that operates on the same + * hierarchy level, use merge(). + * + * + * class Entity + * { + * public $location; + * } + * + * class Location + * { + * public $longitude; + * public $latitude; + * } + * + * $entity = new Entity(); + * $entity->location = new Location(); + * + * $form = new Form('entity', $entity, $validator); + * + * $locationGroup = new FieldGroup('location'); + * $locationGroup->add(new TextField('longitude')); + * $locationGroup->add(new TextField('latitude')); + * + * $form->add($locationGroup); + * + * + * @param FieldInterface $field + */ + public function add(FieldInterface $field) + { + if ($this->isBound()) + { + throw new AlreadyBoundException('You cannot add fields after binding a form'); + } + + $this->fields[$field->getKey()] = $field; + + $field->setParent($this); + $field->setLocale($this->locale); + $field->setGenerator($this->generator); + + if ($this->translator !== null) + { + $field->setTranslator($this->translator); + } + + $data = $this->getTransformedData(); + + // if the property "data" is NULL, getTransformedData() returns an empty + // string + if (!empty($data) && $field->getPropertyPath() !== null) + { + $field->updateFromObject($data); + } + + return $field; + } + + /** + * Merges a field group into this group. The group must have a unique name + * within the group. Otherwise the existing field is overwritten. + * + * Contrary to added groups, merged groups operate on the same object as + * the group they are merged into. + * + * + * class Entity + * { + * public $longitude; + * public $latitude; + * } + * + * $entity = new Entity(); + * + * $form = new Form('entity', $entity, $validator); + * + * $locationGroup = new FieldGroup('location'); + * $locationGroup->add(new TextField('longitude')); + * $locationGroup->add(new TextField('latitude')); + * + * $form->merge($locationGroup); + * + * + * @param FieldGroup $group + */ + public function merge(FieldGroup $group) + { + if ($group->isBound()) + { + throw new AlreadyBoundException('A bound form group cannot be merged'); + } + + foreach ($group as $field) + { + $group->remove($field->getKey()); + $this->add($field); + + if (($path = $group->getPropertyPath()) !== null) + { + $field->setPropertyPath($path.'.'.$field->getPropertyPath()); + } + } + + return $this; + } + + /** + * Removes the field with the given key. + * + * @param string $key + */ + public function remove($key) + { + $this->fields[$key]->setParent(null); + + unset($this->fields[$key]); + } + + /** + * Returns whether a field with the given key exists. + * + * @param string $key + * @return boolean + */ + public function has($key) + { + return isset($this->fields[$key]); + } + + /** + * Returns the field with the given key. + * + * @param string $key + * @return FieldInterface + */ + public function get($key) + { + if (isset($this->fields[$key])) + { + return $this->fields[$key]; + } + + throw new \InvalidArgumentException(sprintf('Field "%s" does not exist.', $key)); + } + + /** + * Returns all fields in this group + * + * @return array + */ + public function getFields() + { + return $this->fields; + } + + /** + * Returns an array of hidden fields from the current schema. + * + * @param boolean $recursive Whether to recur through embedded schemas + * + * @return array + */ + public function getHiddenFields($recursive = true) + { + $fields = array(); + + foreach ($this->fields as $field) + { + if ($field instanceof FieldGroup) + { + if ($recursive) + { + $fields = array_merge($fields, $field->getHiddenFields($recursive)); + } + } + else if ($field->isHidden()) + { + $fields[] = $field; + } + } + + return $fields; + } + + /** + * Initializes the field group with an object to operate on + * + * @see FieldInterface + */ + public function setData($data) + { + parent::setData($data); + + // get transformed data and pass its values to child fields + $data = $this->getTransformedData(); + + if (!empty($data) && !is_array($data) && !is_object($data)) + { + throw new \InvalidArgumentException(sprintf('Expected argument of type object or array, %s given', gettype($data))); + } + + if (!empty($data)) + { + $iterator = new RecursiveFieldsWithPropertyPathIterator($this); + $iterator = new \RecursiveIteratorIterator($iterator); + + foreach ($iterator as $field) + { + $field->updateFromObject($data); + } + } + } + + /** + * Returns the data of the field as it is displayed to the user. + * + * @see FieldInterface + */ + public function getDisplayedData() + { + $values = array(); + + foreach ($this->fields as $key => $field) + { + $values[$key] = $field->getDisplayedData(); + } + + return $values; + } + + /** + * Binds POST data to the field, transforms and validates it. + * + * @param string|array $taintedData The POST data + * @return boolean Whether the form is valid + */ + public function bind($taintedData) + { + if ($taintedData === null) + { + $taintedData = array(); + } + + if (!is_array($taintedData)) + { + throw new UnexpectedTypeException('You must pass an array parameter to the bind() method'); + } + + foreach ($this->fields as $key => $field) + { + if (!isset($taintedData[$key])) + { + $taintedData[$key] = null; + } + } + + foreach ($taintedData as $key => $value) + { + if ($this->has($key)) + { + $this->fields[$key]->bind($value); + } + } + + $data = $this->getTransformedData(); + $iterator = new RecursiveFieldsWithPropertyPathIterator($this); + $iterator = new \RecursiveIteratorIterator($iterator); + + foreach ($iterator as $field) + { + $field->updateObject($data); + } + + // bind and reverse transform the data + parent::bind($data); + + $this->extraFields = array(); + + foreach ($taintedData as $key => $value) + { + if (!$this->has($key)) + { + $this->extraFields[] = $key; + } + } + } + + /** + * Returns whether this form was bound with extra fields + * + * @return boolean + */ + public function isBoundWithExtraFields() + { + // TODO: integrate the field names in the error message + return count($this->extraFields) > 0; + } + + /** + * Returns whether the field is valid. + * + * @return boolean + */ + public function isValid() + { + if (!parent::isValid()) + { + return false; + } + + foreach ($this->fields as $field) + { + if (!$field->isValid()) + { + return false; + } + } + + return true; + } + + /** + * {@inheritDoc} + */ + public function addError($message, PropertyPath $path = null, $type = null) + { + if ($path !== null) + { + if ($type === self::FIELD_ERROR && $path->hasNext()) + { + $path->next(); + + if ($this->has($path->getCurrent()) && !$this->get($path->getCurrent())->isHidden()) + { + $this->get($path->getCurrent())->addError($message, $path, $type); + + return; + } + } + else if ($type === self::DATA_ERROR) + { + $iterator = new RecursiveFieldsWithPropertyPathIterator($this); + $iterator = new \RecursiveIteratorIterator($iterator); + + foreach ($iterator as $field) + { + if (null !== ($fieldPath = $field->getPropertyPath())) + { + $fieldPath->rewind(); + + if ($fieldPath->getCurrent() === $path->getCurrent() && !$field->isHidden()) + { + if ($path->hasNext()) + { + $path->next(); + } + + $field->addError($message, $path, $type); + + return; + } + } + } + } + } + + parent::addError($message); + } + + /** + * Returns whether the field requires a multipart form. + * + * @return boolean + */ + public function isMultipart() + { + foreach ($this->fields as $field) + { + if ($field->isMultipart()) + { + return true; + } + } + + return false; + } + + /** + * Sets the renderer. + * + * @param RendererInterface $renderer + */ + public function setRenderer(RendererInterface $renderer) + { + $this->renderer = $renderer; + } + + /** + * Returns the current renderer. + * + * @return RendererInterface + */ + public function getRenderer() + { + return $this->renderer; + } + + /** + * Delegates the rendering of the field to the renderer set. + * + * @return string The rendered widget + */ + public function render(array $attributes = array()) + { + $this->injectLocaleAndTranslator($this->renderer); + + return $this->renderer->render($this, $attributes); + } + + /** + * Delegates the rendering of the field to the renderer set. + * + * @return string The rendered widget + */ + public function renderErrors() + { + $this->injectLocaleAndTranslator($this->renderer); + + return $this->renderer->renderErrors($this); + } + /** + * Renders hidden form fields. + * + * @param boolean $recursive False will prevent hidden fields from embedded forms from rendering + * + * @return string + */ + public function renderHiddenFields($recursive = true) + { + $output = ''; + + foreach ($this->getHiddenFields($recursive) as $field) + { + $output .= $field->render(); + } + + return $output; + } + + /** + * Returns true if the bound field exists (implements the \ArrayAccess interface). + * + * @param string $key The key of the bound field + * + * @return Boolean true if the widget exists, false otherwise + */ + public function offsetExists($key) + { + return $this->has($key); + } + + /** + * Returns the form field associated with the name (implements the \ArrayAccess interface). + * + * @param string $key The offset of the value to get + * + * @return Field A form field instance + */ + public function offsetGet($key) + { + return $this->get($key); + } + + /** + * Throws an exception saying that values cannot be set (implements the \ArrayAccess interface). + * + * @param string $offset (ignored) + * @param string $value (ignored) + * + * @throws \LogicException + */ + public function offsetSet($key, $field) + { + throw new \LogicException('Use the method add() to add fields'); + } + + /** + * Throws an exception saying that values cannot be unset (implements the \ArrayAccess interface). + * + * @param string $key + * + * @throws \LogicException + */ + public function offsetUnset($key) + { + return $this->remove($key); + } + + /** + * Returns the iterator for this group. + * + * @return \ArrayIterator + */ + public function getIterator() + { + return new \ArrayIterator($this->fields); + } + + /** + * Returns the number of form fields (implements the \Countable interface). + * + * @return integer The number of embedded form fields + */ + public function count() + { + return count($this->fields); + } + + /** + * Sets the locale of this field. + * + * @see Localizable + */ + public function setLocale($locale) + { + parent::setLocale($locale); + + foreach ($this->fields as $field) + { + $field->setLocale($locale); + } + } + + /** + * Sets the translator of this field. + * + * @see Translatable + */ + public function setTranslator(TranslatorInterface $translator) + { + parent::setTranslator($translator); + + foreach ($this->fields as $field) + { + $field->setTranslator($translator); + } + } + + /** + * Distributes the generator among all nested fields + * + * @param HtmlGeneratorInterface $generator + */ + public function setGenerator(HtmlGeneratorInterface $generator) + { + parent::setGenerator($generator); + + // TESTME + foreach ($this->fields as $field) + { + $field->setGenerator($generator); + } + } +} diff --git a/src/Symfony/Components/Form/FieldGroupInterface.php b/src/Symfony/Components/Form/FieldGroupInterface.php new file mode 100644 index 000000000000..50cdea6c1349 --- /dev/null +++ b/src/Symfony/Components/Form/FieldGroupInterface.php @@ -0,0 +1,12 @@ + + */ +interface FieldGroupInterface extends FieldInterface, \ArrayAccess, \Traversable, \Countable +{ +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/FieldInterface.php b/src/Symfony/Components/Form/FieldInterface.php new file mode 100644 index 000000000000..1b4381ac85c7 --- /dev/null +++ b/src/Symfony/Components/Form/FieldInterface.php @@ -0,0 +1,259 @@ + + * @version SVN: $Id: FieldInterface.php 247 2010-02-01 09:24:55Z bernhard $ + */ +interface FieldInterface extends Localizable, Translatable +{ + /** + * Marks a constraint violation in a form field + * @var integer + */ + const FIELD_ERROR = 0; + + /** + * Marks a constraint violation in the data of a form field + * @var integer + */ + const DATA_ERROR = 1; + + /** + * Clones this field. + */ + public function __clone(); + + /** + * Sets the parent field. + * + * @param FieldInterface $parent The parent field + */ + public function setParent(FieldInterface $parent = null); + + /** + * Sets the key by which the field is identified in field groups. + * + * Once this field is nested in a field group, i.e. after setParent() was + * called for the first time, this method should throw an exception. + * + * @param string $key The key of the field + * @throws BadMethodCallException When the field already has a parent + */ + public function setKey($key); + + /** + * Returns the key by which the field is identified in field groups. + * + * @return string The key of the field. + */ + public function getKey(); + + /** + * Returns the name of the field. + * + * @return string When the field has no parent, the name is equal to its + * key. If the field has a parent, the name is composed of + * the parent's name and the field's key, where the field's + * key is wrapped in squared brackets + * (e.g. "parent_name[field_key]") + */ + public function getName(); + + /** + * Returns the ID of the field. + * + * @return string The ID of a field is equal to its name, where all + * sequences of squared brackets are replaced by a single + * underscore (e.g. if the name is "parent_name[field_key]", + * the ID is "parent_name_field_key"). + */ + public function getId(); + + /** + * Sets the property path + * + * The property path determines the property or a sequence of properties + * that a field updates in the data of the field group. + * + * @param string $propertyPath + */ + public function setPropertyPath($propertyPath); + + /** + * Returns the property path of the field + * + * @return PropertyPath + */ + public function getPropertyPath(); + + /** + * Writes a property value of the object into the field + * + * The chosen property is determined by the field's property path. + * + * @param array|object $objectOrArray + */ + public function updateFromObject(&$objectOrArray); + + /** + * Writes a the field value into a property of the object + * + * The chosen property is determined by the field's property path. + * + * @param array|object $objectOrArray + */ + public function updateObject(&$objectOrArray); + + /** + * Returns the normalized data of the field. + * + * @return mixed When the field is not bound, the default data is returned. + * When the field is bound, the normalized bound data is + * returned if the field is valid, null otherwise. + */ + public function getData(); + + /** + * Returns the data of the field as it is displayed to the user. + * + * @return string|array When the field is not bound, the transformed + * default data is returned. When the field is bound, + * the bound data is returned. + */ + public function getDisplayedData(); + + /** + * Sets the default data + * + * @param mixed $default The default data + * @throws UnexpectedTypeException If the default data is invalid + */ + public function setData($default); + + /** + * Binds POST data to the field, transforms and validates it. + * + * @param string|array $taintedData The POST data + * @return boolean Whether the form is valid + * @throws InvalidConfigurationException when the field is not configured + * correctly + */ + public function bind($taintedData); + + /** + * Recursively adds constraint violations to the fields + * + * Violations in the form fields usually have property paths like: + * + * + * iterator[firstName].data + * iterator[firstName].displayedData + * iterator[Address].iterator[street].displayedData + * ... + * + * + * Violations in the form data usually have property paths like: + * + * + * data.firstName + * data.Address.street + * ... + * + * + * @param FieldInterface $field + * @param PropertyPath $path + * @param ConstraintViolation$violation + */ + public function addError($message, PropertyPath $path = null, $type = null); + + /** + * Renders this field. + * + * @param array $attributes The attributes to include in the rendered + * output + * @return string The rendered output of this field + */ + public function render(array $attributes = array()); + + /** + * Renders the errors of this field. + * + * @return string The rendered output of the field errors + */ + public function renderErrors(); + + /** + * Returns whether the field is bound. + * + * @return boolean + */ + public function isBound(); + + /** + * Returns whether the field is valid. + * + * @return boolean + */ + public function isValid(); + + /** + * Returns whether the field requires a multipart form. + * + * @return boolean + */ + public function isMultipart(); + + /** + * Returns whether the field is required to be filled out. + * + * If the field has a parent and the parent is not required, this method + * will always return false. Otherwise the value set with setRequired() + * is returned. + * + * @return boolean + */ + public function isRequired(); + + /** + * Returns whether this field is disabled + * + * The content of a disabled field is displayed, but not allowed to be + * modified. The validation of modified, disabled fields should fail. + * + * Fields whose parents are disabled are considered disabled regardless of + * their own state. + * + * @return boolean + */ + public function isDisabled(); + + /** + * Returns whether the field is hidden + * + * @return boolean + */ + public function isHidden(); + + /** + * Sets whether this field is required to be filled out when submitted. + * + * @param boolean $required + */ + public function setRequired($required); + + /** + * Sets the generator used for rendering HTML. + * + * Usually there is one generator instance shared between all fields of a + * form. + * + * @param string $charset + */ + public function setGenerator(HtmlGeneratorInterface $generator); +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/Form.php b/src/Symfony/Components/Form/Form.php new file mode 100644 index 000000000000..6bd0892db19f --- /dev/null +++ b/src/Symfony/Components/Form/Form.php @@ -0,0 +1,561 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Form represents a form. + * + * A form is composed of a validator schema and a widget form schema. + * + * Form also takes care of Csrf protection by default. + * + * A Csrf secret can be any random string. If set to false, it disables the + * Csrf protection, and if set to null, it forces the form to use the global + * Csrf secret. If the global Csrf secret is also null, then a random one + * is generated on the fly. + * + * @package symfony + * @subpackage form + * @author Fabien Potencier + * @version SVN: $Id: Form.php 245 2010-01-31 22:22:39Z flo $ + */ +class Form extends FieldGroup +{ + protected static $defaultCsrfSecret = null; + protected static $defaultCsrfProtection = false; + protected static $defaultCsrfFieldName = '_csrf_token'; + protected static $defaultLocale = null; + protected static $defaultTranslator = null; + + protected $validator = null; + protected $validationGroups = null; + + private $csrfSecret = null; + private $csrfFieldName = null; + + /** + * Constructor. + * + * @param array $defaults An array of field default values + * @param array $options An array of options + * @param string $defaultCsrfSecret A Csrf secret + */ + public function __construct($name, $object, ValidatorInterface $validator, array $options = array()) + { + $this->generator = new HtmlGenerator(); + $this->validator = $validator; + + parent::__construct($name, $options); + + $this->setData($object); + $this->setCsrfFieldName(self::$defaultCsrfFieldName); + + if (self::$defaultCsrfSecret !== null) + { + $this->setCsrfSecret(self::$defaultCsrfSecret); + } + else + { + $this->setCsrfSecret(md5(__FILE__.php_uname())); + } + + if (self::$defaultCsrfProtection !== false) + { + $this->enableCsrfProtection(); + } + + if (self::$defaultLocale !== null) + { + $this->setLocale(self::$defaultLocale); + } + + if (self::$defaultTranslator !== null) + { + $this->setTranslator(self::$defaultTranslator); + } + } + + /** + * Sets the charset used for rendering HTML + * + * This method overrides the internal HTML generator! If you want to use + * your own generator, use setGenerator() instead. + * + * @param string $charset + */ + public function setCharset($charset) + { + $this->setGenerator(new HtmlGenerator($charset)); + } + + /** + * Sets the validation groups for this form. + * + * @param array|string $validationGroups + */ + public function setValidationGroups($validationGroups) + { + $this->validationGroups = $validationGroups === null ? $validationGroups : (array) $validationGroups; + } + + /** + * Returns the validation groups for this form. + * + * @return array + */ + public function getValidationGroups() + { + return $this->validationGroups; + } + + /** + * Sets the default locale for newly created forms. + * + * @param string $defaultLocale + */ + static public function setDefaultLocale($defaultLocale) + { + self::$defaultLocale = $defaultLocale; + } + + /** + * Returns the default locale for newly created forms. + * + * @return string + */ + static public function getDefaultLocale() + { + return self::$defaultLocale; + } + + /** + * Sets the default translator for newly created forms. + * + * @param TranslatorInterface $defaultTranslator + */ + static public function setDefaultTranslator(TranslatorInterface $defaultTranslator) + { + self::$defaultTranslator = $defaultTranslator; + } + + /** + * Returns the default translator for newly created forms. + * + * @return TranslatorInterface + */ + static public function getDefaultTranslator() + { + return self::$defaultTranslator; + } + + /** + * Binds the form with values and files. + * + * This method is final because it is very easy to break a form when + * overriding this method and adding logic that depends on $taintedFiles. + * You should override doBind() instead where the uploaded files are + * already merged into the data array. + * + * @param array $taintedValues The form data of the $_POST array + * @param array $taintedFiles The form data of the $_FILES array + * @return boolean Whether the form is valid + */ + final public function bind($taintedValues, array $taintedFiles = null) + { + if ($taintedFiles === null) + { + if ($this->isMultipart() && $this->getParent() === null) + { + throw new \InvalidArgumentException('You must provide a files array for multipart forms'); + } + + $taintedFiles = array(); + } + else + { + $taintedFiles = self::convertFileInformation(self::fixPhpFilesArray($taintedFiles)); + } + + $this->doBind(self::deepArrayUnion($taintedValues, $taintedFiles)); + + if ($this->getParent() === null) + { + if ($violations = $this->validator->validate($this, $this->getValidationGroups())) + { + foreach ($violations as $violation) + { + $propertyPath = new PropertyPath($violation->getPropertyPath()); + + if ($propertyPath->getCurrent() == 'data') + { + $type = self::DATA_ERROR; + $propertyPath->next(); // point at the first data element + } + else + { + $type = self::FIELD_ERROR; + } + + $this->addError($violation->getMessage(), $propertyPath, $type); + } + } + } + } + + /** + * Binds the form with the given data. + * + * @param array $taintedData The data to bind to the form + * @return boolean Whether the form is valid + */ + protected function doBind(array $taintedData) + { + parent::bind($taintedData); + } + + /** + * Gets the stylesheet paths associated with the form. + * + * @return array An array of stylesheet paths + */ + public function getStylesheets() + { + return $this->getWidget()->getStylesheets(); + } + + /** + * Gets the JavaScript paths associated with the form. + * + * @return array An array of JavaScript paths + */ + public function getJavaScripts() + { + return $this->getWidget()->getJavaScripts(); + } + + /** + * Returns a CSRF token for the set CSRF secret + * + * If you want to change the algorithm used to compute the token, you + * can override this method. + * + * @param string $secret The secret string to use (null to use the current secret) + * + * @return string A token string + */ + protected function getCsrfToken() + { + return md5($this->csrfSecret.session_id().get_class($this)); + } + + /** + * @return true if this form is CSRF protected + */ + public function isCsrfProtected() + { + return $this->has($this->getCsrfFieldName()); + } + + /** + * Enables CSRF protection for this form. + */ + public function enableCsrfProtection() + { + if (!$this->isCsrfProtected()) + { + $field = new HiddenField($this->getCsrfFieldName(), array( + 'property_path' => null, + )); + $field->setData($this->getCsrfToken()); + $this->add($field); + } + } + + /** + * Disables CSRF protection for this form. + */ + public function disableCsrfProtection() + { + if ($this->isCsrfProtected()) + { + $this->remove($this->getCsrfFieldName()); + } + } + + /** + * Sets the CSRF field name used in this form + * + * @param string $name The CSRF field name + */ + public function setCsrfFieldName($name) + { + $this->csrfFieldName = $name; + } + + /** + * Returns the CSRF field name used in this form + * + * @return string The CSRF field name + */ + public function getCsrfFieldName() + { + return $this->csrfFieldName; + } + + /** + * Sets the CSRF secret used in this form + * + * @param string $secret + */ + public function setCsrfSecret($secret) + { + $this->csrfSecret = $secret; + } + + /** + * Returns the CSRF secret used in this form + * + * @return string + */ + public function getCsrfSecret() + { + return $this->csrfSecret; + } + + /** + * Returns whether the CSRF token is valid + * + * @return boolean + */ + public function isCsrfTokenValid() + { + if (!$this->isCsrfProtected()) + { + return true; + } + else + { + return $this->get($this->getCsrfFieldName())->getDisplayedData() === $this->getCsrfToken(); + } + } + + /** + * Enables CSRF protection for all new forms + */ + static public function enableDefaultCsrfProtection() + { + self::$defaultCsrfProtection = true; + } + + /** + * Disables Csrf protection for all forms. + */ + static public function disableDefaultCsrfProtection() + { + self::$defaultCsrfProtection = false; + } + + /** + * Sets the CSRF field name used in all new CSRF protected forms + * + * @param string $name The CSRF field name + */ + static public function setDefaultCsrfFieldName($name) + { + self::$defaultCsrfFieldName = $name; + } + + /** + * Returns the default CSRF field name + * + * @return string The CSRF field name + */ + static public function getDefaultCsrfFieldName() + { + return self::$defaultCsrfFieldName; + } + + /** + * Sets the CSRF secret used in all new CSRF protected forms + * + * @param string $secret + */ + static public function setDefaultCsrfSecret($secret) + { + self::$defaultCsrfSecret = $secret; + } + + /** + * Returns the default CSRF secret + * + * @return string + */ + static public function getDefaultCsrfSecret() + { + return self::$defaultCsrfSecret; + } + + /** + * Renders the form tag. + * + * This method only renders the opening form tag. + * You need to close it after the form rendering. + * + * This method takes into account the multipart widgets. + * + * @param string $url The URL for the action + * @param array $attributes An array of HTML attributes + * + * @return string An HTML representation of the opening form tag + */ + public function renderFormTag($url, array $attributes = array()) + { + return sprintf('', $this->generator->attributes(array_merge(array( + 'action' => $url, + 'method' => isset($attributes['method']) ? strtolower($attributes['method']) : 'post', + 'enctype' => $this->isMultipart() ? 'multipart/form-data' : null, + ), $attributes))); + } + + /** + * Returns whether the maximum POST size was reached in this request. + * + * @return boolean + */ + public function isPostMaxSizeReached() + { + if (isset($_SERVER['CONTENT_LENGTH'])) + { + $length = (int) $_SERVER['CONTENT_LENGTH']; + $max = trim(ini_get('post_max_size')); + + switch (strtolower(substr($max, -1))) + { + // The 'G' modifier is available since PHP 5.1.0 + case 'g': + $max *= 1024; + case 'm': + $max *= 1024; + case 'k': + $max *= 1024; + } + + return $length > $max; + } + else + { + return false; + } + } + + /** + * Merges two arrays without reindexing numeric keys. + * + * @param array $array1 An array to merge + * @param array $array2 An array to merge + * + * @return array The merged array + */ + static protected function deepArrayUnion($array1, $array2) + { + foreach ($array2 as $key => $value) + { + if (is_array($value) && isset($array1[$key]) && is_array($array1[$key])) + { + $array1[$key] = self::deepArrayUnion($array1[$key], $value); + } + else + { + $array1[$key] = $value; + } + } + + return $array1; + } + + /** + * Fixes a malformed PHP $_FILES array. + * + * PHP has a bug that the format of the $_FILES array differs, depending on + * whether the uploaded file fields had normal field names or array-like + * field names ("normal" vs. "parent[child]"). + * + * This method fixes the array to look like the "normal" $_FILES array. + * + * @param array $data + * @return array + */ + static protected function fixPhpFilesArray(array $data) + { + $fileKeys = array('error', 'name', 'size', 'tmp_name', 'type'); + $keys = array_keys($data); + sort($keys); + + $files = $data; + + if ($fileKeys == $keys && isset($data['name']) && is_array($data['name'])) + { + foreach ($fileKeys as $k) + { + unset($files[$k]); + } + + foreach (array_keys($data['name']) as $key) + { + $files[$key] = self::fixPhpFilesArray(array( + 'error' => $data['error'][$key], + 'name' => $data['name'][$key], + 'type' => $data['type'][$key], + 'tmp_name' => $data['tmp_name'][$key], + 'size' => $data['size'][$key], + )); + } + } + + return $files; + } + + /** + * Converts uploaded files to instances of clsas UploadedFile. + * + * @param array $files A (multi-dimensional) array of uploaded file information + * @return array A (multi-dimensional) array of UploadedFile instances + */ + static protected function convertFileInformation(array $files) + { + $fileKeys = array('error', 'name', 'size', 'tmp_name', 'type'); + + foreach ($files as $key => $data) + { + if (is_array($data)) + { + $keys = array_keys($data); + sort($keys); + + if ($keys == $fileKeys) + { + $files[$key] = new UploadedFile($data['tmp_name'], $data['name'], $data['type'], $data['size'], $data['error']); + } + else + { + $files[$key] = self::convertFileInformation($data); + } + } + } + + return $files; + } +} diff --git a/src/Symfony/Components/Form/HiddenField.php b/src/Symfony/Components/Form/HiddenField.php new file mode 100644 index 000000000000..e2d193881179 --- /dev/null +++ b/src/Symfony/Components/Form/HiddenField.php @@ -0,0 +1,39 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A hidden field + * + * @author Bernhard Schussek + */ +class HiddenField extends InputField +{ + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + return parent::render(array_merge(array( + 'type' => 'hidden', + ), $attributes)); + } + + /** + * {@inheritDoc} + */ + public function isHidden() + { + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/HtmlGenerator.php b/src/Symfony/Components/Form/HtmlGenerator.php new file mode 100644 index 000000000000..77d62b4b9f98 --- /dev/null +++ b/src/Symfony/Components/Form/HtmlGenerator.php @@ -0,0 +1,145 @@ + + * @author Fabien Potencier + */ +class HtmlGenerator implements HtmlGeneratorInterface +{ + /** + * Whether to produce XHTML compliant code + * @var boolean + */ + protected static $xhtml = true; + + /** + * The charset used during generating + * @var string + */ + protected $charset; + + /** + * Sets the charset used for rendering + * + * @param string $charset + */ + public function __construct($charset = 'UTF-8') + { + $this->charset = $charset; + } + + /** + * Sets the XHTML generation flag. + * + * @param bool $boolean true if renderers must be generated as XHTML, false otherwise + */ + static public function setXhtml($boolean) + { + self::$xhtml = (boolean) $boolean; + } + + /** + * Returns whether to generate XHTML tags or not. + * + * @return bool true if renderers must be generated as XHTML, false otherwise + */ + static public function isXhtml() + { + return self::$xhtml; + } + + /** + * {@inheritDoc} + */ + public function tag($tag, $attributes = array()) + { + if (empty($tag)) + { + return ''; + } + + return sprintf('<%s%s%s', $tag, $this->attributes($attributes), self::$xhtml ? ' />' : (strtolower($tag) == 'input' ? '>' : sprintf('>', $tag))); + } + + /** + * {@inheritDoc} + */ + public function contentTag($tag, $content = null, $attributes = array()) + { + if (empty($tag)) + { + return ''; + } + + return sprintf('<%s%s>%s', $tag, $this->attributes($attributes), $content, $tag); + } + + /** + * {@inheritDoc} + */ + public function attribute($name, $value) + { + if (true === $value) + { + return self::$xhtml ? sprintf('%s="%s"', $name, $this->escape($name)) : $this->escape($name); + } + else + { + return sprintf('%s="%s"', $name, $this->escape($value)); + } + } + + /** + * {@inheritDoc} + */ + public function attributes(array $attributes) + { + return implode('', array_map(array($this, 'attributesCallback'), array_keys($attributes), array_values($attributes))); + } + + /** + * Prepares an attribute key and value for HTML representation. + * + * It removes empty attributes, except for the value one. + * + * @param string $name The attribute name + * @param string $value The attribute value + * + * @return string The HTML representation of the HTML key attribute pair. + */ + private function attributesCallback($name, $value) + { + if (false === $value || null === $value || ('' === $value && 'value' != $name)) + { + return ''; + } + else + { + return ' '.$this->attribute($name, $value); + } + } + + /** + * {@inheritDoc} + */ + public function escape($value) + { + return $this->fixDoubleEscape(htmlspecialchars((string) $value, ENT_QUOTES, $this->charset)); + } + + /** + * Fixes double escaped strings. + * + * @param string $escaped string to fix + * + * @return string A single escaped string + */ + protected function fixDoubleEscape($escaped) + { + return preg_replace('/&([a-z]+|(#\d+)|(#x[\da-f]+));/i', '&$1;', $escaped); + } +} diff --git a/src/Symfony/Components/Form/HtmlGeneratorInterface.php b/src/Symfony/Components/Form/HtmlGeneratorInterface.php new file mode 100644 index 000000000000..6d8176298e54 --- /dev/null +++ b/src/Symfony/Components/Form/HtmlGeneratorInterface.php @@ -0,0 +1,63 @@ + + */ +interface HtmlGeneratorInterface +{ + /** + * Escapes a value for safe output in HTML + * + * Double escaping of already-escaped sequences is avoided by this method. + * + * @param string $value The unescaped or partially escaped value + * + * @return string The fully escaped value + */ + public function escape($value); + + /** + * Generates the HTML code for a tag attribute + * + * @param string $name The attribute name + * @param string $value The attribute value + * + * @return string The HTML code of the attribute + */ + public function attribute($name, $value); + + /** + * Generates the HTML code for multiple tag attributes + * + * @param array $attributes An array with attribute names as keys and + * attribute values as elements + * + * @return string The HTML code of the attribute list + */ + public function attributes(array $attributes); + + /** + * Generates the HTML code for a tag without content + * + * @param string $tag The name of the tag + * @param array $attributes The attributes for the tag + * + * @return string The HTML code for the tag + */ + public function tag($tag, $attributes = array()); + + /** + * Generates the HTML code for a tag with content + * + * @param string $tag The name of the tag + * @param string $content The content of the tag + * @param array $attributes The attributes for the tag + * + * @return string The HTML code for the tag + */ + public function contentTag($tag, $content, $attributes = array()); +} diff --git a/src/Symfony/Components/Form/HybridField.php b/src/Symfony/Components/Form/HybridField.php new file mode 100644 index 000000000000..cbf303b9a7f8 --- /dev/null +++ b/src/Symfony/Components/Form/HybridField.php @@ -0,0 +1,101 @@ + + */ +class HybridField extends FieldGroup +{ + const FIELD = 0; + const GROUP = 1; + + protected $mode = self::FIELD; + + /** + * Sets the current mode of the field + * + * Note that you can't switch modes anymore once you have added children to + * this field. + * + * @param integer $mode One of the constants HybridField::FIELD and + * HybridField::GROUP. + */ + public function setFieldMode($mode) + { + if (count($this) > 0 && $mode === self::FIELD) + { + throw new FormException('Switching to mode FIELD is not allowed after adding nested fields'); + } + + $this->mode = $mode; + } + + /** + * {@inheritDoc} + * + * @throws FormException When the field is in mode HybridField::FIELD adding + * subfields is not allowed + */ + public function add(FieldInterface $field) + { + if ($this->mode === self::FIELD) + { + throw new FormException('You cannot add nested fields while in mode FIELD'); + } + + return parent::add($field); + } + + /** + * {@inheritDoc} + */ + public function getDisplayedData() + { + if ($this->mode === self::GROUP) + { + return parent::getDisplayedData(); + } + else + { + return Field::getDisplayedData(); + } + } + + /** + * {@inheritDoc} + */ + public function setData($data) + { + if ($this->mode === self::GROUP) + { + parent::setData($data); + } + else + { + Field::setData($data); + } + } + + /** + * {@inheritDoc} + */ + public function bind($data) + { + if ($this->mode === self::GROUP) + { + parent::bind($data); + } + else + { + Field::bind($data); + } + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/InputField.php b/src/Symfony/Components/Form/InputField.php new file mode 100644 index 000000000000..726b2bf9360e --- /dev/null +++ b/src/Symfony/Components/Form/InputField.php @@ -0,0 +1,24 @@ + + */ +abstract class InputField extends Field +{ + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + return $this->generator->tag('input', array_merge(array( + 'id' => $this->getId(), + 'name' => $this->getName(), + 'value' => $this->getDisplayedData(), + 'disabled' => $this->isDisabled(), + ), $attributes)); + } +} diff --git a/src/Symfony/Components/Form/IntegerField.php b/src/Symfony/Components/Form/IntegerField.php new file mode 100644 index 000000000000..02be60184985 --- /dev/null +++ b/src/Symfony/Components/Form/IntegerField.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A localized field for entering integers. + * + * @author Bernhard Schussek + */ +class IntegerField extends NumberField +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->addOption('precision', 0); + + parent::configure(); + } + + /** + * {@inheritDoc} + */ + public function getData() + { + return (int)parent::getData(); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/Iterator/RecursiveFieldsWithPropertyPathIterator.php b/src/Symfony/Components/Form/Iterator/RecursiveFieldsWithPropertyPathIterator.php new file mode 100644 index 000000000000..5bad7c5b35d9 --- /dev/null +++ b/src/Symfony/Components/Form/Iterator/RecursiveFieldsWithPropertyPathIterator.php @@ -0,0 +1,23 @@ +current()); + } + + public function hasChildren() + { + return $this->current() instanceof FieldGroupInterface && $this->current()->getPropertyPath() === null; + } +} diff --git a/src/Symfony/Components/Form/Localizable.php b/src/Symfony/Components/Form/Localizable.php new file mode 100644 index 000000000000..8684ba2420fc --- /dev/null +++ b/src/Symfony/Components/Form/Localizable.php @@ -0,0 +1,18 @@ + + */ +interface Localizable +{ + /** + * Sets the locale of the class. + * + * @param string $locale + */ + public function setLocale($locale); +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/MoneyField.php b/src/Symfony/Components/Form/MoneyField.php new file mode 100644 index 000000000000..780fc9429d76 --- /dev/null +++ b/src/Symfony/Components/Form/MoneyField.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A localized field for entering money values + * + * @author Bernhard Schussek + */ +class MoneyField extends NumberField +{ + /** + * Stores patterns for different locales and cultures + * + * A pattern decides which currency symbol is displayed and where it is in + * relation to the number. + * + * @var array + */ + protected static $patterns = array(); + + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->addOption('precision', 2); + $this->addOption('divisor', 1); + $this->addOption('currency'); + + parent::configure(); + + $this->setValueTransformer(new MoneyToLocalizedStringTransformer(array( + 'precision' => $this->getOption('precision'), + 'grouping' => $this->getOption('grouping'), + 'divisor' => $this->getOption('divisor'), + ))); + } + + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + $input = parent::render($attributes); + + if ($this->getOption('currency')) + { + return str_replace('%widget%', $input, $this->getPattern($this->locale, $this->getOption('currency'))); + } + else + { + return $input; + } + } + + /** + * Returns the pattern for this locale + * + * The pattern contains the placeholder "%widget%" where the HTML tag should + * be inserted + * + * @param string $locale + */ + protected static function getPattern($locale, $currency) + { + if (!isset(self::$patterns[$locale])) + { + self::$patterns[$locale] = array(); + } + + if (!isset(self::$patterns[$locale][$currency])) + { + $format = new \NumberFormatter($locale, \NumberFormatter::CURRENCY); + $pattern = $format->formatCurrency('123', $currency); + + // the spacings between currency symbol and number are ignored, because + // a single space leads to better readability in combination with input + // fields + + // the regex also considers non-break spaces (0xC2 or 0xA0 in UTF-8) + + preg_match('/^([^\s\xc2\xa0]*)[\s\xc2\xa0]*123[,.]00[\s\xc2\xa0]*([^\s\xc2\xa0]*)$/', $pattern, $matches); + + if (!empty($matches[1])) + { + self::$patterns[$locale] = $matches[1].' %widget%'; + } + else if (!empty($matches[2])) + { + self::$patterns[$locale] = '%widget% '.$matches[2]; + } + else + { + self::$patterns[$locale] = '%widget%'; + } + } + + return self::$patterns[$locale]; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/NumberField.php b/src/Symfony/Components/Form/NumberField.php new file mode 100644 index 000000000000..11a1e0b23c46 --- /dev/null +++ b/src/Symfony/Components/Form/NumberField.php @@ -0,0 +1,47 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A localized field for entering numbers. + * + * @author Bernhard Schussek + */ +class NumberField extends InputField +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + // default precision is locale specific (usually around 3) + $this->addOption('precision'); + $this->addOption('grouping', false); + + $this->setValueTransformer(new NumberToLocalizedStringTransformer(array( + 'precision' => $this->getOption('precision'), + 'grouping' => $this->getOption('grouping'), + ))); + } + + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + return parent::render(array_merge(array( + 'type' => 'text', + ), $attributes)); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/PasswordField.php b/src/Symfony/Components/Form/PasswordField.php new file mode 100644 index 000000000000..d45eef38bef3 --- /dev/null +++ b/src/Symfony/Components/Form/PasswordField.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A field for entering a password. + * + * @author Bernhard Schussek + */ +class PasswordField extends TextField +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + parent::configure(); + + $this->addOption('always_empty', true); + } + + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + return parent::render(array_merge(array( + 'value' => $this->getOption('always_empty') && !$this->isBound() ? '' : $this->getDisplayedData(), + 'type' => 'password', + ), $attributes)); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/PercentField.php b/src/Symfony/Components/Form/PercentField.php new file mode 100644 index 000000000000..0a746b063d66 --- /dev/null +++ b/src/Symfony/Components/Form/PercentField.php @@ -0,0 +1,46 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A localized field for entering percentage values. + * + * @author Bernhard Schussek + */ +class PercentField extends NumberField +{ + const FRACTIONAL = 'fractional'; + const INTEGER = 'integer'; + + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->addOption('precision', 0); + $this->addOption('type', self::FRACTIONAL); + + $this->setValueTransformer(new PercentToLocalizedStringTransformer(array( + 'precision' => $this->getOption('precision'), + 'type' => $this->getOption('type'), + ))); + } + + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + return parent::render($attributes).' %'; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/PropertyPath.php b/src/Symfony/Components/Form/PropertyPath.php new file mode 100644 index 000000000000..fbfed0338bc0 --- /dev/null +++ b/src/Symfony/Components/Form/PropertyPath.php @@ -0,0 +1,162 @@ + + */ +class PropertyPath +{ + /** + * The current index of the traversal + * @var integer + */ + protected $currentIndex = 0; + + /** + * The elements of the property path + * @var array + */ + protected $elements = array(); + + /** + * Contains a boolean for each property in $elements denoting whether this + * element is a property. It is an index otherwise. + * @var array + */ + protected $isProperty = array(); + + /** + * String representation of the path + * @var string + */ + protected $string; + + /** + * Parses the given property path + * + * @param string $propertyPath + */ + public function __construct($propertyPath) + { + if (empty($propertyPath)) + { + throw new InvalidPropertyPathException('The property path must not be empty'); + } + + $this->string = $propertyPath; + $position = 0; + $remaining = $propertyPath; + + // first element is evaluated differently - no leading dot for properties + $pattern = '/^((\w+)|\[(\w+)\])(.*)/'; + + while (preg_match($pattern, $remaining, $matches)) + { + if (!empty($matches[2])) + { + $this->elements[] = $matches[2]; + $this->isProperty[] = true; + } + else + { + $this->elements[] = $matches[3]; + $this->isProperty[] = false; + } + + $position += strlen($matches[1]); + $remaining = $matches[4]; + $pattern = '/^(\.(\w+)|\[(\w+)\])(.*)/'; + } + + if (!empty($remaining)) + { + throw new InvalidPropertyPathException(sprintf( + 'Could not parse property path "%s". Unexpected token "%s" at position %d', + $propertyPath, + $remaining{0}, + $position + )); + } + } + + /** + * Returns the string representation of the property path + * + * @return string + */ + public function __toString() + { + return $this->string; + } + + /** + * Returns the current element of the path + * + * @return string + */ + public function getCurrent() + { + return $this->elements[$this->currentIndex]; + } + + /** + * Returns whether the current element is a property + * + * @return boolean + */ + public function isProperty() + { + return $this->isProperty[$this->currentIndex]; + } + + /** + * Returns whether the currente element is an array index + * + * @return boolean + */ + public function isIndex() + { + return !$this->isProperty(); + } + + /** + * Returns whether there is a next element in the path + * + * @return boolean + */ + public function hasNext() + { + return isset($this->elements[$this->currentIndex + 1]); + } + + /** + * Sets the internal cursor to the next element in the path + * + * Use hasNext() to verify whether there is a next element before calling this + * method, otherwise an exception will be thrown. + * + * @throws OutOfBoundsException If there is no next element + */ + public function next() + { + if (!$this->hasNext()) + { + throw new \OutOfBoundsException('There is no next element in the path'); + } + + ++$this->currentIndex; + } + + /** + * Sets the internal cursor to the first element in the path + */ + public function rewind() + { + $this->currentIndex = 0; + } +} diff --git a/src/Symfony/Components/Form/RadioField.php b/src/Symfony/Components/Form/RadioField.php new file mode 100644 index 000000000000..e899c90b5c83 --- /dev/null +++ b/src/Symfony/Components/Form/RadioField.php @@ -0,0 +1,33 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A radio field for selecting boolean values. + * + * @author Bernhard Schussek + */ +class RadioField extends ToggleField +{ + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + return parent::render(array_merge(array( + 'type' => 'radio', + 'name' => $this->getParent() ? $this->getParent()->getName() : $this->getName(), + ), $attributes)); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/Renderer/Renderer.php b/src/Symfony/Components/Form/Renderer/Renderer.php new file mode 100644 index 000000000000..68bb57bb0a3e --- /dev/null +++ b/src/Symfony/Components/Form/Renderer/Renderer.php @@ -0,0 +1,91 @@ + 'all', '/another/file.css' => 'screen,print') + * + * @return array An array of stylesheet paths + */ + public function getStylesheets() + { + return array(); + } + + /** + * Gets the JavaScript paths associated with the renderer. + * + * @return array An array of JavaScript paths + */ + public function getJavaScripts() + { + return array(); + } + + /** + * {@inheritDoc} + */ + public function renderErrors(FieldInterface $field) + { + $html = ''; + + if ($field->hasErrors()) + { + $html .= "
    \n"; + + foreach ($field->getErrors() as $error) + { + $html .= "
  • " . $error . "
  • \n"; + } + + $html .= "
\n"; + } + + return $html; + } + + /** + * {@inheritDoc} + */ + public function setTranslator(TranslatorInterface $translator) + { + // TODO + } + + /** + * {@inheritDoc} + */ + public function setLocale($locale) + { + // TODO + } + + /** + * {@inheritDoc} + */ + public function setGenerator(HtmlGeneratorInterface $generator) + { + $this->generator = $generator; + } +} diff --git a/src/Symfony/Components/Form/Renderer/RendererInterface.php b/src/Symfony/Components/Form/Renderer/RendererInterface.php new file mode 100644 index 000000000000..6a028c2cee81 --- /dev/null +++ b/src/Symfony/Components/Form/Renderer/RendererInterface.php @@ -0,0 +1,45 @@ + + */ +interface RendererInterface extends Localizable, Translatable +{ + /** + * Sets the generator used for rendering the HTML + * + * @param HtmlGeneratorInterface $generator + */ + public function setGenerator(HtmlGeneratorInterface $generator); + + /** + * Returns the textual representation of the given field. + * + * @param FieldInterface $field The form field + * @param array $attributes The attributes to include in the + * rendered output + * @return string The rendered output + * @throws InvalidArgumentException If the $field is not instance of the + * expected class + */ + public function render(FieldInterface $field, array $attributes = array()); + + /** + * Returns the textual representation of the errors of the given field. + * + * @param FieldInterface $field The form field + * @return string The rendered output + * @throws InvalidArgumentException If the $field is not instance of the + * expected class + */ + public function renderErrors(FieldInterface $field); +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/Renderer/TableRenderer.php b/src/Symfony/Components/Form/Renderer/TableRenderer.php new file mode 100644 index 000000000000..cbeb3b71d441 --- /dev/null +++ b/src/Symfony/Components/Form/Renderer/TableRenderer.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * Renders a field group as HTML table + * + * @author Bernhard Schussek + */ +class TableRenderer extends Renderer +{ + /** + * {@inheritDoc} + */ + public function render(FieldInterface $group, array $attributes = array()) + { + $html = "\n"; + + foreach ($group as $field) + { + $label = self::humanize($field->getKey()); + + $html .= "\n"; + $html .= "\n"; + $html .= ""; + $html .= "\n"; + } + + $html .= "
\n"; + if ($field->hasErrors()) + { + $html .= $field->renderErrors()."\n"; + } + $html .= $field->render()."\n"; + $html .= "
\n"; + + return $html; + } + + protected static function humanize($text) + { + return ucfirst(strtolower(str_replace('_', ' ', $text))); + } +} diff --git a/src/Symfony/Components/Form/RepeatedField.php b/src/Symfony/Components/Form/RepeatedField.php new file mode 100644 index 000000000000..710f8bb96f2c --- /dev/null +++ b/src/Symfony/Components/Form/RepeatedField.php @@ -0,0 +1,88 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A field for repeated input of values + * + * @author Bernhard Schussek + */ +class RepeatedField extends FieldGroup +{ + /** + * The prototype for the inner fields + * @var FieldInterface + */ + protected $prototype; + + /** + * Repeats the given field twice to verify the user's input + * + * @param FieldInterface $innerField + */ + public function __construct(FieldInterface $innerField, array $options = array()) + { + $this->prototype = $innerField; + + parent::__construct($innerField->getKey(), $options); + } + + /** + * {@inheritDoc} + */ + protected function configure() + { + $field = clone $this->prototype; + $field->setKey('first'); + $field->setPropertyPath('first'); + $this->add($field); + + $field = clone $this->prototype; + $field->setKey('second'); + $field->setPropertyPath('second'); + $this->add($field); + } + + /** + * Returns whether both entered values are equal + * + * @return bool + */ + public function isFirstEqualToSecond() + { + return $this->get('first')->getData() === $this->get('second')->getData(); + } + + /** + * Sets the values of both fields to this value + * + * @param mixed $data + */ + public function setData($data) + { + parent::setData(array('first' => $data, 'second' => $data)); + } + + /** + * Return only value of first password field. + * + * @return string The password. + */ + public function getData() + { + if ($this->isBound() && $this->isFirstEqualToSecond()) + { + return $this->get('first')->getData(); + } + + return null; + } +} diff --git a/src/Symfony/Components/Form/Resources/config/validation.xml b/src/Symfony/Components/Form/Resources/config/validation.xml new file mode 100644 index 000000000000..8ffaafee2704 --- /dev/null +++ b/src/Symfony/Components/Form/Resources/config/validation.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Symfony/Components/Form/Resources/i18n/messages.en.xml b/src/Symfony/Components/Form/Resources/i18n/messages.en.xml new file mode 100644 index 000000000000..eccf989f1e48 --- /dev/null +++ b/src/Symfony/Components/Form/Resources/i18n/messages.en.xml @@ -0,0 +1,23 @@ + + + + + + Symfony.Form.FieldGroup.extraFieldsMessage + This field group should not contain extra fields + + + Symfony.Form.Form.postMaxSizeMessage + The uploaded file was too large. Please try to upload a smaller file. + + + Symfony.Form.Form.csrfInvalidMessage + The CSRF token is invalid + + + Symfony.Form.RepeatedField.invalidMessage + The two values should be equal + + + + \ No newline at end of file diff --git a/src/Symfony/Components/Form/TextField.php b/src/Symfony/Components/Form/TextField.php new file mode 100644 index 000000000000..7b0528f7abd1 --- /dev/null +++ b/src/Symfony/Components/Form/TextField.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A text input field. + * + * @author Bernhard Schussek + */ +class TextField extends InputField +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + parent::configure(); + + $this->addOption('max_length'); + } + + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + return parent::render(array_merge(array( + 'type' => 'text', + 'maxlength' => $this->getOption('max_length'), + ), $attributes)); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/TextareaField.php b/src/Symfony/Components/Form/TextareaField.php new file mode 100644 index 000000000000..94678b213d55 --- /dev/null +++ b/src/Symfony/Components/Form/TextareaField.php @@ -0,0 +1,36 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * A textarea field + * + * @author Bernhard Schussek + */ +class TextareaField extends Field +{ + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + $content = $this->generator->escape($this->getDisplayedData()); + + return $this->generator->contentTag('textarea', $content, array_merge(array( + 'id' => $this->getId(), + 'name' => $this->getName(), + 'rows' => 30, + 'cols' => 4, + ), $attributes)); + } +} diff --git a/src/Symfony/Components/Form/TimeField.php b/src/Symfony/Components/Form/TimeField.php new file mode 100644 index 000000000000..81c2750f4e39 --- /dev/null +++ b/src/Symfony/Components/Form/TimeField.php @@ -0,0 +1,157 @@ +addOption('hours', range(0, 23)); + $this->addOption('minutes', range(0, 59)); + $this->addOption('seconds', range(0, 59)); + $this->addOption('widget', self::CHOICE, self::$widgets); + $this->addOption('type', self::DATETIME, self::$types); + $this->addOption('data_timezone', 'UTC'); + $this->addOption('user_timezone', 'UTC'); + $this->addOption('with_seconds', false); + + if ($this->getOption('widget') == self::INPUT) + { + $this->add(new TextField('hour', array('max_length' => 2))); + $this->add(new TextField('minute', array('max_length' => 2))); + + if ($this->getOption('with_seconds')) + { + $this->add(new TextField('second', array('max_length' => 2))); + } + } + else + { + $this->add(new ChoiceField('hour', array( + 'choices' => $this->generatePaddedChoices($this->getOption('hours'), 2), + ))); + $this->add(new ChoiceField('minute', array( + 'choices' => $this->generatePaddedChoices($this->getOption('minutes'), 2), + ))); + + if ($this->getOption('with_seconds')) + { + $this->add(new ChoiceField('second', array( + 'choices' => $this->generatePaddedChoices($this->getOption('seconds'), 2), + ))); + } + } + + $transformers = array(); + + if ($this->getOption('type') == self::STRING) + { + $transformers[] = new StringToDateTimeTransformer(array( + 'format' => 'H:i:s', + 'input_timezone' => $this->getOption('data_timezone'), + 'output_timezone' => $this->getOption('data_timezone'), + )); + } + else if ($this->getOption('type') == self::TIMESTAMP) + { + $transformers[] = new TimestampToDateTimeTransformer(array( + 'input_timezone' => $this->getOption('data_timezone'), + 'output_timezone' => $this->getOption('data_timezone'), + )); + } + else if ($this->getOption('type') === self::RAW) + { + $transformers[] = new ReversedTransformer(new DateTimeToArrayTransformer(array( + 'input_timezone' => $this->getOption('data_timezone'), + 'output_timezone' => $this->getOption('data_timezone'), + 'fields' => array('hour', 'minute', 'second'), + ))); + } + + $transformers[] = new DateTimeToArrayTransformer(array( + 'input_timezone' => $this->getOption('data_timezone'), + 'output_timezone' => $this->getOption('user_timezone'), + // if the field is rendered as choice field, the values should be trimmed + // of trailing zeros to render the selected choices correctly + 'pad' => $this->getOption('widget') == self::INPUT, + )); + + $this->setValueTransformer(new ValueTransformerChain($transformers)); + } + + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + if ($this->getOption('widget') == self::INPUT) + { + $attributes = array_merge(array( + 'size' => '1', + ), $attributes); + } + + $html = $this->get('hour')->render($attributes); + $html .= ':' . $this->get('minute')->render($attributes); + + if ($this->getOption('with_seconds')) + { + $html .= ':' . $this->get('second')->render($attributes); + } + + return $html; + } + + /** + * Generates an array of choices for the given values + * + * If the values are shorter than $padLength characters, they are padded with + * zeros on the left side. + * + * @param array $values The available choices + * @param integer $padLength The length to pad the choices + * @return array An array with the input values as keys and the + * padded values as values + */ + protected function generatePaddedChoices(array $values, $padLength) + { + $choices = array(); + + foreach ($values as $value) + { + $choices[$value] = str_pad($value, $padLength, '0', STR_PAD_LEFT); + } + + return $choices; + } +} diff --git a/src/Symfony/Components/Form/TimezoneField.php b/src/Symfony/Components/Form/TimezoneField.php new file mode 100644 index 000000000000..c3f6bc35e90d --- /dev/null +++ b/src/Symfony/Components/Form/TimezoneField.php @@ -0,0 +1,85 @@ +addOption('choices', self::getTimezoneChoices()); + + parent::configure(); + } + + /** + * Preselects the server timezone if the field is empty and required + * + * {@inheritDoc} + */ + public function getDisplayedData() + { + $data = parent::getDisplayedData(); + + if ($data == null && $this->isRequired()) + { + $data = date_default_timezone_get(); + } + + return $data; + } + + /** + * Returns the timezone choices + * + * The choices are generated from the ICU function + * \DateTimeZone::listIdentifiers(). They are cached during a single request, + * so multiple timezone fields on the same page don't lead to unnecessary + * overhead. + * + * @return array The timezone choices + */ + protected static function getTimezoneChoices() + { + if (count(self::$timezones) == 0) + { + foreach (\DateTimeZone::listIdentifiers() as $timezone) + { + $parts = explode('/', $timezone); + + if (count($parts) > 2) + { + $region = $parts[0]; + $name = $parts[1].' - '.$parts[2]; + } + else if (count($parts) > 1) + { + $region = $parts[0]; + $name = $parts[1]; + } + else + { + $region = 'Other'; + $name = $parts[0]; + } + + if (!isset(self::$timezones[$region])) + { + self::$timezones[$region] = array(); + } + + self::$timezones[$region][$timezone] = str_replace('_', ' ', $name); + } + } + + return self::$timezones; + } +} diff --git a/src/Symfony/Components/Form/ToggleField.php b/src/Symfony/Components/Form/ToggleField.php new file mode 100644 index 000000000000..2d4543026c02 --- /dev/null +++ b/src/Symfony/Components/Form/ToggleField.php @@ -0,0 +1,59 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * An input field for selecting boolean values. + * + * @author Bernhard Schussek + */ +abstract class ToggleField extends InputField +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->addOption('value'); + $this->addOption('label'); + $this->addOption('translate_label', false); + + $this->setValueTransformer(new BooleanToStringTransformer()); + } + + /** + * {@inheritDoc} + */ + public function render(array $attributes = array()) + { + $html = parent::render(array_merge(array( + 'value' => $this->getOption('value'), + 'checked' => ((string)$this->getDisplayedData() !== '' && $this->getDisplayedData() !== 0), + ), $attributes)); + + if ($label = $this->getOption('label')) + { + if ($this->getOption('translate_label')) + { + $label = $this->translate($label); + } + + $html .= ' '.$this->generator->contentTag('label', $label, array( + 'for' => $this->getId(), + )); + } + + return $html; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/Translatable.php b/src/Symfony/Components/Form/Translatable.php new file mode 100644 index 000000000000..0c28a3430ffc --- /dev/null +++ b/src/Symfony/Components/Form/Translatable.php @@ -0,0 +1,20 @@ + + */ +interface Translatable +{ + /** + * Sets the translator unit of the class. + * + * @param TranslatorInterface $translator + */ + public function setTranslator(TranslatorInterface $translator); +} diff --git a/src/Symfony/Components/Form/ValueTransformer/BaseDateTimeTransformer.php b/src/Symfony/Components/Form/ValueTransformer/BaseDateTimeTransformer.php new file mode 100644 index 000000000000..6a6fc92bbe1d --- /dev/null +++ b/src/Symfony/Components/Form/ValueTransformer/BaseDateTimeTransformer.php @@ -0,0 +1,44 @@ + + */ +abstract class BaseValueTransformer extends Configurable implements ValueTransformerInterface +{ + /** + * The locale of this transformer as accepted by the class Locale + * @var string + */ + protected $locale; + + /** + * Constructor. + * + * @param array $options An array of options + * + * @throws \InvalidArgumentException when a option is not supported + * @throws \RuntimeException when a required option is not given + */ + public function __construct(array $options = array()) + { + $this->locale = \Locale::getDefault(); + + parent::__construct($options); + } + + /** + * {@inheritDoc} + */ + public function setLocale($locale) + { + $this->locale = $locale; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/ValueTransformer/BooleanToStringTransformer.php b/src/Symfony/Components/Form/ValueTransformer/BooleanToStringTransformer.php new file mode 100644 index 000000000000..b6951b6e42e5 --- /dev/null +++ b/src/Symfony/Components/Form/ValueTransformer/BooleanToStringTransformer.php @@ -0,0 +1,45 @@ + + * @author Florian Eckerstorfer + */ +class BooleanToStringTransformer extends BaseValueTransformer +{ + /** + * Transforms a boolean into a string. + * + * @param boolean $value Boolean value. + * @return string String value. + */ + public function transform($value) + { + if (!is_bool($value)) + { + throw new \InvalidArgumentException(sprintf('Expected argument of type boolean but got %s.', gettype($value))); + } + + return true === $value ? '1' : ''; + } + + /** + * Transforms a string into a boolean. + * + * @param string $value String value. + * @return boolean Boolean value. + */ + public function reverseTransform($value) + { + if (!is_string($value)) + { + throw new \InvalidArgumentException(sprintf('Expected argument of type string but got %s.', gettype($value))); + } + + return $value !== ''; + } + +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/ValueTransformer/DateTimeToArrayTransformer.php b/src/Symfony/Components/Form/ValueTransformer/DateTimeToArrayTransformer.php new file mode 100644 index 000000000000..6cd10ff4915a --- /dev/null +++ b/src/Symfony/Components/Form/ValueTransformer/DateTimeToArrayTransformer.php @@ -0,0 +1,110 @@ + + * @author Florian Eckerstorfer + */ +class DateTimeToArrayTransformer extends BaseDateTimeTransformer +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + parent::configure(); + + $this->addOption('input_timezone', 'UTC'); + $this->addOption('output_timezone', 'UTC'); + $this->addOption('pad', false); + $this->addOption('fields', array('year', 'month', 'day', 'hour', 'minute', 'second')); + } + + /** + * Transforms a normalized date into a localized date string/array. + * + * @param DateTime $dateTime Normalized date. + * @return string|array Localized date array. + */ + public function transform($dateTime) + { + if (!$dateTime instanceof \DateTime) + { + throw new \InvalidArgumentException('Expected value of type \DateTime'); + } + + $inputTimezone = $this->getOption('input_timezone'); + $outputTimezone = $this->getOption('output_timezone'); + + if ($inputTimezone != $outputTimezone) + { + $dateTime->setTimezone(new \DateTimeZone($outputTimezone)); + } + + $result = array_intersect_key(array( + 'year' => $dateTime->format('Y'), + 'month' => $dateTime->format('m'), + 'day' => $dateTime->format('d'), + 'hour' => $dateTime->format('H'), + 'minute' => $dateTime->format('i'), + 'second' => $dateTime->format('s'), + ), array_flip($this->getOption('fields'))); + + if (!$this->getOption('pad')) + { + foreach ($result as &$entry) + { + $entry = (int)$entry; + } + } + + return $result; + } + + /** + * Transforms a localized date string/array into a normalized date. + * + * @param array $value Localized date string/array + * @return DateTime Normalized date + */ + public function reverseTransform($value) + { + $inputTimezone = $this->getOption('input_timezone'); + $outputTimezone = $this->getOption('output_timezone'); + + if (!is_array($value)) + { + throw new \InvalidArgumentException(sprintf('Expected argument of type array, %s given', gettype($value))); + } + + $dateTime = new \DateTime(sprintf( + '%s-%s-%s %s:%s:%s %s', + isset($value['year']) ? $value['year'] : 1970, + isset($value['month']) ? $value['month'] : 1, + isset($value['day']) ? $value['day'] : 1, + isset($value['hour']) ? $value['hour'] : 0, + isset($value['minute']) ? $value['minute'] : 0, + isset($value['second']) ? $value['second'] : 0, + $outputTimezone + )); + + if ($inputTimezone != $outputTimezone) + { + $dateTime->setTimezone(new \DateTimeZone($inputTimezone)); + } + + return $dateTime; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/ValueTransformer/DateTimeToLocalizedStringTransformer.php b/src/Symfony/Components/Form/ValueTransformer/DateTimeToLocalizedStringTransformer.php new file mode 100644 index 000000000000..5d4a0b8fadcf --- /dev/null +++ b/src/Symfony/Components/Form/ValueTransformer/DateTimeToLocalizedStringTransformer.php @@ -0,0 +1,122 @@ + + * @author Florian Eckerstorfer + */ +class DateTimeToLocalizedStringTransformer extends BaseDateTimeTransformer +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + parent::configure(); + + $this->addOption('date_format', self::MEDIUM); + $this->addOption('time_format', self::SHORT); + $this->addOption('input_timezone', 'UTC'); + $this->addOption('output_timezone', 'UTC'); + + if (!in_array($this->getOption('date_format'), self::$formats, true)) + { + throw new \InvalidArgumentException(sprintf('The option "date_format" is expected to be one of "%s". Is "%s"', implode('", "', self::$formats), $this->getOption('time_format'))); + } + + if (!in_array($this->getOption('time_format'), self::$formats, true)) + { + throw new \InvalidArgumentException(sprintf('The option "time_format" is expected to be one of "%s". Is "%s"', implode('", "', self::$formats), $this->getOption('time_format'))); + } + } + + /** + * Transforms a normalized date into a localized date string/array. + * + * @param DateTime $dateTime Normalized date. + * @return string|array Localized date string/array. + */ + public function transform($dateTime) + { + if (!$dateTime instanceof \DateTime) + { + throw new \InvalidArgumentException('Expected value of type \DateTime'); + } + + $inputTimezone = $this->getOption('input_timezone'); + + // convert time to UTC before passing it to the formatter + if ($inputTimezone != 'UTC') + { + $dateTime->setTimezone(new \DateTimeZone('UTC')); + } + + $value = $this->getIntlDateFormatter()->format((int)$dateTime->format('U')); + + if (intl_get_error_code() != 0) + { + throw new TransformationFailedException(intl_get_error_message()); + } + + return $value; + } + + /** + * Transforms a localized date string/array into a normalized date. + * + * @param string|array $value Localized date string/array + * @return DateTime Normalized date + */ + public function reverseTransform($value) + { + $inputTimezone = $this->getOption('input_timezone'); + + if (!is_string($value)) + { + throw new \InvalidArgumentException(sprintf('Expected argument of type string, %s given', gettype($value))); + } + + $timestamp = $this->getIntlDateFormatter()->parse($value); + + if (intl_get_error_code() != 0) + { + throw new TransformationFailedException(intl_get_error_message()); + } + + // read timestamp into DateTime object - the formatter delivers in UTC + $dateTime = new \DateTime(sprintf('@%s UTC', $timestamp)); + + if ($inputTimezone != 'UTC') + { + $dateTime->setTimezone(new \DateTimeZone($inputTimezone)); + } + + return $dateTime; + } + + /** + * Returns a preconfigured IntlDateFormatter instance + * + * @return \IntlDateFormatter + */ + protected function getIntlDateFormatter() + { + $dateFormat = $this->getIntlFormatConstant($this->getOption('date_format')); + $timeFormat = $this->getIntlFormatConstant($this->getOption('time_format')); + $timezone = $this->getOption('output_timezone'); + + return new \IntlDateFormatter($this->locale, $dateFormat, $timeFormat, $timezone); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/ValueTransformer/MoneyToLocalizedStringTransformer.php b/src/Symfony/Components/Form/ValueTransformer/MoneyToLocalizedStringTransformer.php new file mode 100644 index 000000000000..6ff97a71dc85 --- /dev/null +++ b/src/Symfony/Components/Form/ValueTransformer/MoneyToLocalizedStringTransformer.php @@ -0,0 +1,54 @@ + + * @author Florian Eckerstorfer + */ +class MoneyToLocalizedStringTransformer extends NumberToLocalizedStringTransformer +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->addOption('grouping', true); + $this->addOption('precision', 2); + $this->addOption('divisor', 1); + + parent::configure(); + } + + /** + * Transforms a normalized format into a localized money string. + * + * @param number $value Normalized number + * @return string Localized money string. + */ + public function transform($value) + { + if (!is_numeric($value)) + { + throw new \InvalidArgumentException(sprintf('Numeric argument expected, %s given', gettype($value))); + } + + return parent::transform($value / $this->getOption('divisor')); + } + + /** + * Transforms a localized money string into a normalized format. + * + * @param string $value Localized money string + * @return number Normalized number + */ + public function reverseTransform($value) + { + return parent::reverseTransform($value) * $this->getOption('divisor'); + } + +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/ValueTransformer/NumberToLocalizedStringTransformer.php b/src/Symfony/Components/Form/ValueTransformer/NumberToLocalizedStringTransformer.php new file mode 100644 index 000000000000..07106200a31f --- /dev/null +++ b/src/Symfony/Components/Form/ValueTransformer/NumberToLocalizedStringTransformer.php @@ -0,0 +1,92 @@ + + * @author Florian Eckerstorfer + */ +class NumberToLocalizedStringTransformer extends BaseValueTransformer +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->addOption('precision', null); + $this->addOption('grouping', false); + + parent::configure(); + } + + /** + * Transforms a number type into localized number. + * + * @param number $value Number value. + * @return string Localized value. + */ + public function transform($value) + { + if (!is_numeric($value)) + { + throw new \InvalidArgumentException(sprintf('Numeric argument expected, %s given', gettype($value))); + } + + $formatter = $this->getNumberFormatter(); + $value = $formatter->format($value); + + if (intl_is_failure($formatter->getErrorCode())) + { + throw new TransformationFailedException($formatter->getErrorMessage()); + } + + return $value; + } + + /** + * Transforms a localized number into an integer or float + * + * @param string $value + */ + public function reverseTransform($value) + { + if (!is_string($value)) + { + throw new \InvalidArgumentException(sprintf('Expected argument of type string, %s given', gettype($value))); + } + + $formatter = $this->getNumberFormatter(); + $value = $formatter->parse($value); + + if (intl_is_failure($formatter->getErrorCode())) + { + throw new TransformationFailedException($formatter->getErrorMessage()); + } + + return $value; + } + + /** + * Returns a preconfigured \NumberFormatter instance + * + * @return \NumberFormatter + */ + protected function getNumberFormatter() + { + $formatter = new \NumberFormatter($this->locale, \NumberFormatter::DECIMAL); + + if ($this->getOption('precision') !== null) + { + $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->getOption('precision')); + } + + $formatter->setAttribute(\NumberFormatter::GROUPING_USED, $this->getOption('grouping')); + + return $formatter; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/ValueTransformer/PercentToLocalizedStringTransformer.php b/src/Symfony/Components/Form/ValueTransformer/PercentToLocalizedStringTransformer.php new file mode 100644 index 000000000000..17a258cd052c --- /dev/null +++ b/src/Symfony/Components/Form/ValueTransformer/PercentToLocalizedStringTransformer.php @@ -0,0 +1,112 @@ + + * @author Florian Eckerstorfer + */ +class PercentToLocalizedStringTransformer extends BaseValueTransformer +{ + const FRACTIONAL = 'fractional'; + const INTEGER = 'integer'; + + protected static $types = array( + self::FRACTIONAL, + self::INTEGER, + ); + + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->addOption('type', self::FRACTIONAL); + $this->addOption('precision', 0); + + if (!in_array($this->getOption('type'), self::$types, true)) + { + throw new \InvalidArgumentException(sprintf('The option "type" is expected to be one of "%s"', implode('", "', self::$types))); + } + + parent::configure(); + } + + /** + * Transforms between a normalized format (integer or float) into a percentage value. + * + * @param number $value Normalized value. + * @return number Percentage value. + */ + public function transform($value) + { + if (!is_numeric($value)) + { + throw new \InvalidArgumentException(sprintf('Numeric argument expected, %s given', gettype($value))); + } + + if (self::FRACTIONAL == $this->getOption('type')) + { + $value *= 100; + } + + $formatter = $this->getNumberFormatter(); + $value = $formatter->format($value); + + if (intl_is_failure($formatter->getErrorCode())) + { + throw new TransformationFailedException($formatter->getErrorMessage()); + } + + // replace the UTF-8 non break spaces + return $value; + } + + /** + * Transforms between a percentage value into a normalized format (integer or float). + * + * @param number $value Percentage value. + * @return number Normalized value. + */ + public function reverseTransform($value) + { + if (!is_string($value)) + { + throw new \InvalidArgumentException(sprintf('Expected argument of type string, %s given', gettype($value))); + } + + $formatter = $this->getNumberFormatter(); + // replace normal spaces so that the formatter can read them + $value = $formatter->parse(str_replace(' ', ' ', $value)); + + if (intl_is_failure($formatter->getErrorCode())) + { + throw new TransformationFailedException($formatter->getErrorMessage()); + } + + if (self::FRACTIONAL == $this->getOption('type')) + { + $value /= 100; + } + + return $value; + } + + /** + * Returns a preconfigured \NumberFormatter instance + * + * @return \NumberFormatter + */ + protected function getNumberFormatter() + { + $formatter = new \NumberFormatter($this->locale, \NumberFormatter::DECIMAL); + + $formatter->setAttribute(\NumberFormatter::FRACTION_DIGITS, $this->getOption('precision')); + + return $formatter; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/ValueTransformer/ReversedTransformer.php b/src/Symfony/Components/Form/ValueTransformer/ReversedTransformer.php new file mode 100644 index 000000000000..68181358c47e --- /dev/null +++ b/src/Symfony/Components/Form/ValueTransformer/ReversedTransformer.php @@ -0,0 +1,54 @@ + + */ +class ReversedTransformer implements ValueTransformerInterface +{ + /** + * The reversed transformer + * @var ValueTransformerInterface + */ + protected $reversedTransformer; + + /** + * Reverses this transformer + * + * @param ValueTransformerInterface $innerTransformer + */ + public function __construct(ValueTransformerInterface $reversedTransformer) + { + $this->reversedTransformer = $reversedTransformer; + } + + /** + * {@inheritDoc} + */ + public function transform($value) + { + return $this->reversedTransformer->reverseTransform($value); + } + + /** + * {@inheritDoc} + */ + public function reverseTransform($value) + { + return $this->reversedTransformer->transform($value); + } + + /** + * {@inheritDoc} + */ + public function setLocale($locale) + { + $this->reversedTransformer->setLocale($locale); + } +} diff --git a/src/Symfony/Components/Form/ValueTransformer/StringToDateTimeTransformer.php b/src/Symfony/Components/Form/ValueTransformer/StringToDateTimeTransformer.php new file mode 100644 index 000000000000..e8dc5bf32766 --- /dev/null +++ b/src/Symfony/Components/Form/ValueTransformer/StringToDateTimeTransformer.php @@ -0,0 +1,71 @@ + + * @author Florian Eckerstorfer + */ +class StringToDateTimeTransformer extends BaseValueTransformer +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->addOption('input_timezone', 'UTC'); + $this->addOption('output_timezone', 'UTC'); + $this->addOption('format', 'Y-m-d H:i:s'); + } + + /** + * Transforms a date string in the configured timezone into a DateTime object + * + * @param string $value A value as produced by PHP's date() function + * @return DateTime A DateTime object + */ + public function transform($value) + { + $inputTimezone = $this->getOption('input_timezone'); + $outputTimezone = $this->getOption('output_timezone'); + + try + { + $dateTime = new \DateTime("$value $inputTimezone"); + + if ($inputTimezone != $outputTimezone) + { + $dateTime->setTimeZone(new \DateTimeZone($outputTimezone)); + } + + return $dateTime; + } + catch (\Exception $e) + { + throw new \InvalidArgumentException('Expected a valid date string. ' . $e->getMessage(), 0, $e); + } + } + + /** + * Transforms a DateTime object into a date string with the configured format + * and timezone + * + * @param DateTime $value A DateTime object + * @return string A value as produced by PHP's date() function + */ + public function reverseTransform($value) + { + if (!$value instanceof \DateTime) + { + throw new \InvalidArgumentException('Expected value of type \DateTime'); + } + + $value->setTimezone(new \DateTimeZone($this->getOption('input_timezone'))); + + return $value->format($this->getOption('format')); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/ValueTransformer/TimestampToDateTimeTransformer.php b/src/Symfony/Components/Form/ValueTransformer/TimestampToDateTimeTransformer.php new file mode 100644 index 000000000000..4e126402d4b6 --- /dev/null +++ b/src/Symfony/Components/Form/ValueTransformer/TimestampToDateTimeTransformer.php @@ -0,0 +1,69 @@ + + * @author Florian Eckerstorfer + */ +class TimestampToDateTimeTransformer extends BaseValueTransformer +{ + /** + * {@inheritDoc} + */ + protected function configure() + { + $this->addOption('input_timezone', 'UTC'); + $this->addOption('output_timezone', 'UTC'); + } + + /** + * Transforms a timestamp in the configured timezone into a DateTime object + * + * @param string $value A value as produced by PHP's date() function + * @return DateTime A DateTime object + */ + public function transform($value) + { + $inputTimezone = $this->getOption('input_timezone'); + $outputTimezone = $this->getOption('output_timezone'); + + try + { + $dateTime = new \DateTime("@$value $inputTimezone"); + + if ($inputTimezone != $outputTimezone) + { + $dateTime->setTimezone(new \DateTimeZone($outputTimezone)); + } + + return $dateTime; + } + catch (\Exception $e) + { + throw new \InvalidArgumentException('Expected a valid timestamp. ' . $e->getMessage(), 0, $e); + } + } + + /** + * Transforms a DateTime object into a timestamp in the configured timezone + * + * @param DateTime $value A DateTime object + * @return integer A timestamp + */ + public function reverseTransform($value) + { + if (!$value instanceof \DateTime) + { + throw new \InvalidArgumentException('Expected value of type \DateTime'); + } + + $value->setTimezone(new \DateTimeZone($this->getOption('input_timezone'))); + + return (int)$value->format('U'); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Form/ValueTransformer/TransformationFailedException.php b/src/Symfony/Components/Form/ValueTransformer/TransformationFailedException.php new file mode 100644 index 000000000000..555515705b0a --- /dev/null +++ b/src/Symfony/Components/Form/ValueTransformer/TransformationFailedException.php @@ -0,0 +1,12 @@ + + */ +class TransformationFailedException extends \RuntimeException +{ +} diff --git a/src/Symfony/Components/Form/ValueTransformer/ValueTransformerChain.php b/src/Symfony/Components/Form/ValueTransformer/ValueTransformerChain.php new file mode 100644 index 000000000000..c92e47a5e4c6 --- /dev/null +++ b/src/Symfony/Components/Form/ValueTransformer/ValueTransformerChain.php @@ -0,0 +1,81 @@ + + */ +class ValueTransformerChain implements ValueTransformerInterface +{ + /** + * The value transformers + * @var array + */ + protected $transformers; + + /** + * Uses the given value transformers to transform values + * + * @param array $transformers + */ + public function __construct(array $transformers) + { + $this->transformers = $transformers; + } + + /** + * Passes the value through the transform() method of all nested transformers + * + * The transformers receive the value in the same order as they were passed + * to the constructor. Each transformer receives the result of the previous + * transformer as input. The output of the last transformer is returned + * by this method. + * + * @param mixed $value The original value + * @return mixed The transformed value + */ + public function transform($value) + { + foreach ($this->transformers as $transformer) + { + $value = $transformer->transform($value); + } + + return $value; + } + + /** + * Passes the value through the reverseTransform() method of all nested + * transformers + * + * The transformers receive the value in the reverse order as they were passed + * to the constructor. Each transformer receives the result of the previous + * transformer as input. The output of the last transformer is returned + * by this method. + * + * @param mixed $value The transformed value + * @return mixed The reverse-transformed value + */ + public function reverseTransform($value) + { + for ($i = count($this->transformers) - 1; $i >= 0; --$i) + { + $value = $this->transformers[$i]->reverseTransform($value); + } + + return $value; + } + + /** + * {@inheritDoc} + */ + public function setLocale($locale) + { + foreach ($this->transformers as $transformer) + { + $transformer->setLocale($locale); + } + } +} diff --git a/src/Symfony/Components/Form/ValueTransformer/ValueTransformerInterface.php b/src/Symfony/Components/Form/ValueTransformer/ValueTransformerInterface.php new file mode 100644 index 000000000000..b98c1379c95c --- /dev/null +++ b/src/Symfony/Components/Form/ValueTransformer/ValueTransformerInterface.php @@ -0,0 +1,38 @@ + + */ +interface ValueTransformerInterface extends Localizable +{ + /** + * Transforms a value from the original representation to a transformed + * representation. + * + * @param mixed $value The value in the original representation + * @return mixed The value in the transformed representation + * @throws InvalidArgument Exception when the argument is no string + * @throws ValueTransformer Exception when the transformation fails + */ + public function transform($value); + + /** + * Transforms a value from the transformed representation to its original + * representation. + * + * This method must be able to deal with null values. + * + * @param mixed $value The value in the transformed representation + * @return mixed The value in the original representation + * @throws InvalidArgument Exception when the argument is not of the + * expected type + * @throws ValueTransformer Exception when the transformation fails + */ + public function reverseTransform($value); +} \ No newline at end of file diff --git a/src/Symfony/Components/I18N/TranslatorInterface.php b/src/Symfony/Components/I18N/TranslatorInterface.php new file mode 100644 index 000000000000..f134536ebd1e --- /dev/null +++ b/src/Symfony/Components/I18N/TranslatorInterface.php @@ -0,0 +1,22 @@ + + */ +interface TranslatorInterface +{ + /** + * Translates a given text string. + * + * @param string $text The text to translate + * @param array $parameters The parameters to inject into the text + * @param string $locale The locale of the translated text. If null, + * the preconfigured locale of the translator + * or the system's default culture is used. + */ + public function translate($text, array $parameters = array(), $locale = null); +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraint.php b/src/Symfony/Components/Validator/Constraint.php new file mode 100644 index 000000000000..70395b6fe925 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraint.php @@ -0,0 +1,178 @@ + + */ +class Constraint +{ + const DEFAULT_GROUP = 'Default'; + + public $groups = self::DEFAULT_GROUP; + + /** + * Initializes the constraint with options + * + * You should pass an associative array. The keys should be the names of + * existing properties in this class. The values should be the value for these + * properties. + * + * Alternatively you can override the method defaultOption() to return the + * name of an existing property. If no associative array is passed, this + * property is set instead. + * + * You can force that certain options are set by overriding + * requiredOptions() to return the names of these options. If any + * option is not set here, an exception is thrown. + * + * @param mixed $options The options (as associative array) + * or the value for the default + * option (any other type) + * @throws InvalidOptionsException When you pass the names of non-existing + * options + * @throws MissingOptionsException When you don't pass any of the options + * returned by requiredOptions() + * @throws ConstraintDefinitionException When you don't pass an associative + * array, but defaultOption() returns + * NULL + */ + public function __construct($options = null) + { + $invalidOptions = array(); + $missingOptions = array_flip((array)$this->requiredOptions()); + + if (is_array($options) && count($options) == 1 && isset($options['value'])) + { + $options = $options['value']; + } + + if (is_array($options) && count($options) > 0 && is_string(key($options))) + { + foreach ($options as $option => $value) + { + if (property_exists($this, $option)) + { + $this->$option = $value; + unset($missingOptions[$option]); + } + else + { + $invalidOptions[] = $option; + } + } + } + else if ($options) + { + $option = $this->defaultOption(); + + if (is_null($option)) + { + throw new ConstraintDefinitionException( + sprintf('No default option is configured for constraint %s', get_class($this)) + ); + } + + if (property_exists($this, $option)) + { + $this->$option = $options; + unset($missingOptions[$option]); + } + else + { + $invalidOptions[] = $option; + } + } + + if (count($invalidOptions) > 0) + { + throw new InvalidOptionsException( + sprintf('The options "%s" do not exist in constraint %s', implode('", "', $invalidOptions), get_class($this)), + $invalidOptions + ); + } + + if (count($missingOptions) > 0) + { + throw new MissingOptionsException( + sprintf('The options "%s" must be set for constraint %s', implode('", "', array_keys($missingOptions)), get_class($this)), + array_keys($missingOptions) + ); + } + + $this->groups = (array)$this->groups; + } + + /** + * Unsupported operation. + */ + public function __set($option, $value) + { + throw new InvalidOptionsException(sprintf('The option "%s" does not exist in constraint %s', $option, get_class($this)), array($option)); + } + + /** + * Adds the given group if this constraint is in the Default group + * + * @param string $group + */ + public function addImplicitGroupName($group) + { + if (in_array(Constraint::DEFAULT_GROUP, $this->groups) && !in_array($group, $this->groups)) + { + $this->groups[] = $group; + } + } + + /** + * Returns the name of the default option + * + * Override this method to define a default option. + * + * @return string + * @see __construct() + */ + public function defaultOption() + { + return null; + } + + /** + * Returns the name of the required options + * + * Override this method if you want to define required options. + * + * @return array + * @see __construct() + */ + public function requiredOptions() + { + return array(); + } + + /** + * Returns the name of the class that validates this constraint + * + * By default, this is the fully qualified name of the constraint class + * suffixed with "Validator". You can override this method to change that + * behaviour. + * + * @return string + */ + public function validatedBy() + { + return get_class($this) . 'Validator'; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/ConstraintValidator.php b/src/Symfony/Components/Validator/ConstraintValidator.php new file mode 100644 index 000000000000..d04f180540c0 --- /dev/null +++ b/src/Symfony/Components/Validator/ConstraintValidator.php @@ -0,0 +1,33 @@ +context = $context; + $this->messageTemplate = ''; + $this->messageParameters = array(); + } + + public function getMessageTemplate() + { + return $this->messageTemplate; + } + + public function getMessageParameters() + { + return $this->messageParameters; + } + + protected function setMessage($template, array $parameters = array()) + { + $this->messageTemplate = $template; + $this->messageParameters = $parameters; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/ConstraintValidatorFactory.php b/src/Symfony/Components/Validator/ConstraintValidatorFactory.php new file mode 100644 index 000000000000..d8c34c524747 --- /dev/null +++ b/src/Symfony/Components/Validator/ConstraintValidatorFactory.php @@ -0,0 +1,23 @@ +validatedBy(); + + if (!isset($this->validators[$className])) + { + $this->validators[$className] = new $className(); + } + + return $this->validators[$className]; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/ConstraintValidatorFactoryInterface.php b/src/Symfony/Components/Validator/ConstraintValidatorFactoryInterface.php new file mode 100644 index 000000000000..7304a3eaaa68 --- /dev/null +++ b/src/Symfony/Components/Validator/ConstraintValidatorFactoryInterface.php @@ -0,0 +1,10 @@ +message = $message; + $this->root = $root; + $this->propertyPath = $propertyPath; + $this->invalidValue = $invalidValue; + } + + public function getMessage() + { + return $this->message; + } + + public function getRoot() + { + return $this->root; + } + + public function getPropertyPath() + { + return $this->propertyPath; + } + + public function getInvalidValue() + { + return $this->invalidValue; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/ConstraintViolationList.php b/src/Symfony/Components/Validator/ConstraintViolationList.php new file mode 100644 index 000000000000..6512dafbad09 --- /dev/null +++ b/src/Symfony/Components/Validator/ConstraintViolationList.php @@ -0,0 +1,49 @@ +violations as $violation) + { + $param = $violation->getMessageParameters(); + $message = str_replace(array_keys($param), $param, $violation->getMessageTemplate()); + $string .= <<getRoot()}.{$violation->getPropertyPath()}: + $message + +EOF; + } + + return $string; + } + + public function add(ConstraintViolation $violation) + { + $this->violations[] = $violation; + } + + public function addAll(ConstraintViolationList $violations) + { + foreach ($violations->violations as $violation) + { + $this->violations[] = $violation; + } + } + + public function getIterator() + { + return new \ArrayIterator($this->violations); + } + + public function count() + { + return count($this->violations); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/All.php b/src/Symfony/Components/Validator/Constraints/All.php new file mode 100644 index 000000000000..458f6c2e7dd6 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/All.php @@ -0,0 +1,18 @@ +context->getGraphWalker(); + $group = $this->context->getGroup(); + $propertyPath = $this->context->getPropertyPath(); + + // cannot simply cast to array, because then the object is converted to an + // array instead of wrapped inside + $constraints = is_array($constraint->constraints) ? $constraint->constraints : array($constraint->constraints); + + foreach ($value as $key => $element) + { + foreach ($constraints as $constr) + { + $walker->walkConstraint($constr, $element, $group, $propertyPath.'['.$key.']'); + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/AssertFalse.php b/src/Symfony/Components/Validator/Constraints/AssertFalse.php new file mode 100644 index 000000000000..f83e3e613fab --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/AssertFalse.php @@ -0,0 +1,8 @@ +setMessage($constraint->message); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/AssertTrue.php b/src/Symfony/Components/Validator/Constraints/AssertTrue.php new file mode 100644 index 000000000000..29ce4d04ff93 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/AssertTrue.php @@ -0,0 +1,8 @@ +setMessage($constraint->message); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/AssertType.php b/src/Symfony/Components/Validator/Constraints/AssertType.php new file mode 100644 index 000000000000..1d02a5ccbb61 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/AssertType.php @@ -0,0 +1,25 @@ +type == 'boolean' ? 'bool' : $constraint->type; + $function = 'is_' . $type; + + if (function_exists($function) && call_user_func($function, $value)) + { + return true; + } + else if ($value instanceof $constraint->type) + { + return true; + } + + $this->setMessage($constraint->message, array( + 'value' => $value, + 'type' => $constraint->type, + )); + + return false; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/Blank.php b/src/Symfony/Components/Validator/Constraints/Blank.php new file mode 100644 index 000000000000..b6bbdd879eb9 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/Blank.php @@ -0,0 +1,8 @@ +setMessage($constraint->message, array('value' => $value)); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/Choice.php b/src/Symfony/Components/Validator/Constraints/Choice.php new file mode 100644 index 000000000000..bede7d586c1d --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/Choice.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +class Choice extends \Symfony\Components\Validator\Constraint +{ + public $choices; + public $callback; + public $multiple = false; + public $min = null; + public $max = null; + public $message = 'Symfony.Validator.Choice.message'; + public $minMessage = 'Symfony.Validator.Choice.minMessage'; + public $maxMessage = 'Symfony.Validator.Choice.maxMessage'; + + /** + * {@inheritDoc} + */ + public function defaultOption() + { + return 'choices'; + } +} diff --git a/src/Symfony/Components/Validator/Constraints/ChoiceValidator.php b/src/Symfony/Components/Validator/Constraints/ChoiceValidator.php new file mode 100644 index 000000000000..c828deb44684 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/ChoiceValidator.php @@ -0,0 +1,101 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/** + * ChoiceValidator validates that the value is one of the expected values. + * + * @author Fabien Potencier + * @author Florian Eckerstorfer + * @author Bernhard Schussek + */ +class ChoiceValidator extends ConstraintValidator +{ + public function isValid($value, Constraint $constraint) + { + if (!$constraint->choices && !$constraint->callback) + { + throw new ConstraintDefinitionException('Either "choices" or "callback" must be specified on constraint Choice'); + } + + if ($value === null) + { + return true; + } + + if ($constraint->multiple && !is_array($value)) + { + throw new UnexpectedTypeException($value, 'array'); + } + + if ($constraint->callback) + { + if (is_callable(array($this->context->getCurrentClass(), $constraint->callback))) + { + $choices = call_user_func(array($this->context->getCurrentClass(), $constraint->callback)); + } + else if (is_callable($constraint->callback)) + { + $choices = call_user_func($constraint->callback); + } + else + { + throw new ConstraintDefinitionException('The Choice constraint expects a valid callback'); + } + } + else + { + $choices = $constraint->choices; + } + + if ($constraint->multiple) + { + foreach ($value as $_value) + { + if (!in_array($_value, $choices, true)) + { + $this->setMessage($constraint->message, array('value' => $_value)); + + return false; + } + } + + $count = count($value); + + if ($constraint->min !== null && $count < $constraint->min) + { + $this->setMessage($constraint->minMessage, array('limit' => $constraint->min)); + + return false; + } + + if ($constraint->max !== null && $count > $constraint->max) + { + $this->setMessage($constraint->maxMessage, array('limit' => $constraint->max)); + + return false; + } + } + elseif (!in_array($value, $choices, true)) + { + $this->setMessage($constraint->message, array('value' => $value)); + + return false; + } + + return true; + } +} diff --git a/src/Symfony/Components/Validator/Constraints/Collection.php b/src/Symfony/Components/Validator/Constraints/Collection.php new file mode 100644 index 000000000000..4724b2e8e01a --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/Collection.php @@ -0,0 +1,17 @@ +context->getGraphWalker(); + $group = $this->context->getGroup(); + $propertyPath = $this->context->getPropertyPath(); + + $missingFields = array(); + $extraFields = array(); + + foreach ($value as $field => $fieldValue) + { + $extraFields[$field] = $fieldValue; + } + + foreach ($constraint->fields as $field => $constraints) + { + if (array_key_exists($field, $value)) + { + // cannot simply cast to array, because then the object is converted to an + // array instead of wrapped inside + $constraints = is_array($constraints) ? $constraints : array($constraints); + + foreach ($constraints as $constr) + { + $walker->walkConstraint($constr, $value[$field], $group, $propertyPath.'['.$field.']'); + } + + unset($extraFields[$field]); + } + else + { + $missingFields[] = $field; + } + } + + if (count($extraFields) > 0 && !$constraint->allowExtraFields) + { + $this->setMessage($constraint->extraFieldsMessage, array( + 'fields' => '"'.implode('", "', array_keys($extraFields)).'"' + )); + + return false; + } + + if (count($missingFields) > 0 && !$constraint->allowMissingFields) + { + $this->setMessage($constraint->missingFieldsMessage, array( + 'fields' => '"'.implode('", "', $missingFields).'"' + )); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/Date.php b/src/Symfony/Components/Validator/Constraints/Date.php new file mode 100644 index 000000000000..9e774f5dabd6 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/Date.php @@ -0,0 +1,8 @@ +setMessage($constraint->message, array('value' => $value)); + + return false; + } + + return checkdate($matches[2], $matches[3], $matches[1]); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/DateValidator.php b/src/Symfony/Components/Validator/Constraints/DateValidator.php new file mode 100644 index 000000000000..7b749c5c8552 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/DateValidator.php @@ -0,0 +1,36 @@ +setMessage($constraint->message, array('value' => $value)); + + return false; + } + + return checkdate($matches[2], $matches[3], $matches[1]); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/Email.php b/src/Symfony/Components/Validator/Constraints/Email.php new file mode 100644 index 000000000000..28c38153c87a --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/Email.php @@ -0,0 +1,9 @@ +setMessage($constraint->message, array('value' => $value)); + + return false; + } + + if ($constraint->checkMX) + { + $host = substr($value, strpos($value, '@')); + + if (!$this->checkMX($host)) + { + $this->setMessage($constraint->message, array('value' => $value)); + + return false; + } + } + + return true; + } + + /** + * Check DNA Records for MX type (from Doctrine EmailValidator) + * + * @param string $host Host name + * @return boolean + * @licence This software consists of voluntary contributions made by many individuals + * and is licensed under the LGPL. For more information, see + * . + */ + private function checkMX($host) + { + // We have different behavior here depending of OS and PHP version + if (strtolower(substr(PHP_OS, 0, 3)) == 'win' && version_compare(PHP_VERSION, '5.3.0', '<')) { + $output = array(); + + @exec('nslookup -type=MX '.escapeshellcmd($host) . ' 2>&1', $output); + + if (empty($output)) + { + throw new ValidatorError('Unable to execute DNS lookup. Are you sure PHP can call exec()?'); + } + + foreach ($output as $line) + { + if (preg_match('/^'.$host.'/', $line)) + { + return true; + } + } + + return false; + } + else if (function_exists('checkdnsrr')) + { + return checkdnsrr($host, 'MX'); + } + + throw new ValidatorError('Could not retrieve DNS record information. Remove check_mx = true to prevent this warning'); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/File.php b/src/Symfony/Components/Validator/Constraints/File.php new file mode 100644 index 000000000000..bddc34435440 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/File.php @@ -0,0 +1,13 @@ +getPath() : (string)$value; + + if (!file_exists($path)) + { + $this->setMessage($constraint->notFoundMessage, array('file' => $path)); + + return false; + } + + if (!is_readable($path)) + { + $this->setMessage($constraint->notReadableMessage, array('file' => $path)); + + return false; + } + + if ($constraint->maxSize) + { + if (ctype_digit((string)$constraint->maxSize)) + { + $size = filesize($path); + $limit = $constraint->maxSize; + $suffix = ' bytes'; + } + else if (preg_match('/^(\d)k$/', $constraint->maxSize, $matches)) + { + $size = round(filesize($path) / 1000, 2); + $limit = $matches[1]; + $suffix = ' kB'; + } + else if (preg_match('/^(\d)M$/', $constraint->maxSize, $matches)) + { + $size = round(filesize($path) / 1000000, 2); + $limit = $matches[1]; + $suffix = ' MB'; + } + else + { + throw new ConstraintDefinitionException(sprintf('"%s" is not a valid maximum size', $constraint->maxSize)); + } + + if ($size > $limit) + { + $this->setMessage($constraint->maxSizeMessage, array( + 'size' => $size . $suffix, + 'limit' => $limit . $suffix, + 'file' => $path, + )); + + return false; + } + } + + if ($constraint->mimeTypes) + { + if (!$value instanceof File) + { + throw new ConstraintValidationException(); + } + + if (!in_array($value->getMimeType(), (array)$constraint->mimeTypes)) + { + $this->setMessage($constraint->mimeTypesMessage, array( + 'type' => '"'.$value->getMimeType().'"', + 'types' => '"'.implode('", "', (array)$constraint->mimeTypes).'"', + 'file' => $path, + )); + + return false; + } + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/Max.php b/src/Symfony/Components/Validator/Constraints/Max.php new file mode 100644 index 000000000000..c0d90ed23e75 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/Max.php @@ -0,0 +1,25 @@ +charset) : strlen($value); + + if ($length > $constraint->limit) + { + $this->setMessage($constraint->message, array( + 'value' => $value, + 'limit' => $constraint->limit, + )); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/MaxValidator.php b/src/Symfony/Components/Validator/Constraints/MaxValidator.php new file mode 100644 index 000000000000..96e2e580b120 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/MaxValidator.php @@ -0,0 +1,35 @@ + $constraint->limit) + { + $this->setMessage($constraint->message, array( + 'value' => $value, + 'limit' => $constraint->limit, + )); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/Min.php b/src/Symfony/Components/Validator/Constraints/Min.php new file mode 100644 index 000000000000..ca42ff9b1929 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/Min.php @@ -0,0 +1,25 @@ +charset) : strlen($value); + + if ($length < $constraint->limit) + { + $this->setMessage($constraint->message, array( + 'value' => $value, + 'limit' => $constraint->limit, + )); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/MinValidator.php b/src/Symfony/Components/Validator/Constraints/MinValidator.php new file mode 100644 index 000000000000..d1580cf9c2cd --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/MinValidator.php @@ -0,0 +1,35 @@ +limit) + { + $this->setMessage($constraint->message, array( + 'value' => $value, + 'limit' => $constraint->limit, + )); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/NotBlank.php b/src/Symfony/Components/Validator/Constraints/NotBlank.php new file mode 100644 index 000000000000..b507500b3d86 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/NotBlank.php @@ -0,0 +1,8 @@ +setMessage($constraint->message); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/NotNull.php b/src/Symfony/Components/Validator/Constraints/NotNull.php new file mode 100644 index 000000000000..f66598ddc59f --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/NotNull.php @@ -0,0 +1,8 @@ +setMessage($constraint->message); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/Null.php b/src/Symfony/Components/Validator/Constraints/Null.php new file mode 100644 index 000000000000..55a647c88aec --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/Null.php @@ -0,0 +1,8 @@ +setMessage($constraint->message, array('value' => $value)); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/Regex.php b/src/Symfony/Components/Validator/Constraints/Regex.php new file mode 100644 index 000000000000..d2f9ae0c7e53 --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/Regex.php @@ -0,0 +1,26 @@ +match && !preg_match($constraint->pattern, $value)) + || + (!$constraint->match && preg_match($constraint->pattern, $value)) + ) + { + $this->setMessage($constraint->message, array('value' => $value)); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/Time.php b/src/Symfony/Components/Validator/Constraints/Time.php new file mode 100644 index 000000000000..195801ea26da --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/Time.php @@ -0,0 +1,8 @@ +setMessage($constraint->message, array('value' => $value)); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/Url.php b/src/Symfony/Components/Validator/Constraints/Url.php new file mode 100644 index 000000000000..20bc46a8569c --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/Url.php @@ -0,0 +1,9 @@ +protocols)); + + if (!preg_match($pattern, $value)) + { + $this->setMessage($constraint->message, array('value' => $value)); + + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/Valid.php b/src/Symfony/Components/Validator/Constraints/Valid.php new file mode 100644 index 000000000000..fe3bdedcac4b --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/Valid.php @@ -0,0 +1,9 @@ +context->getGraphWalker(); + $group = $this->context->getGroup(); + $propertyPath = $this->context->getPropertyPath(); + $factory = $this->context->getClassMetadataFactory(); + + if (is_array($value)) + { + foreach ($value as $key => $element) + { + $walker->walkConstraint($constraint, $element, $group, $propertyPath.'['.$key.']'); + } + } + else if (!is_object($value)) + { + throw new UnexpectedTypeException($value, 'object or array'); + } + else if ($constraint->class && !$value instanceof $constraint->class) + { + $this->setMessage($constraint->message, array('class' => $constraint->class)); + + return false; + } + else + { + $metadata = $factory->getClassMetadata(get_class($value)); + $walker->walkClass($metadata, $value, $group, $propertyPath); + } + + return true; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Constraints/Validation.php b/src/Symfony/Components/Validator/Constraints/Validation.php new file mode 100644 index 000000000000..05999bf9f31a --- /dev/null +++ b/src/Symfony/Components/Validator/Constraints/Validation.php @@ -0,0 +1,13 @@ +constraints = $constraints['value']; + } +} diff --git a/src/Symfony/Components/Validator/DependencyInjectionValidatorFactory.php b/src/Symfony/Components/Validator/DependencyInjectionValidatorFactory.php new file mode 100644 index 000000000000..52f2856afce4 --- /dev/null +++ b/src/Symfony/Components/Validator/DependencyInjectionValidatorFactory.php @@ -0,0 +1,58 @@ +container = $container; + } + + /** + * Gets contraint validator service, setting it if it doesn't exist + * Throws exception if validator service is not instance of ConstraintValidatorInterface + * @param Constraint $constraint + * @return ConstraintValidatorInterface + * @throws \LogicException + */ + public function getInstance(Constraint $constraint) + { + $className = $constraint->validatedBy(); + $id = $this->getServiceIdFromClass($className); + + if (!$this->container->hasService($id)) + { + $this->container->setService($id, new $className()); + } + + $validator = $this->container->getService($id); + + if (!$validator instanceof ConstraintValidatorInterface) { + throw new \LogicException('Service ' . $id . ' is not instance of ConstraintValidatorInterface'); + } + + return $validator; + } + + /** + * Gets service id, corresponding to full class name of ConstraintValidator + * @param string $className + * @return string + */ + protected function getServiceIdFromClass($className) + { + return str_replace('\\', '.', $className); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Exception/ConstraintDefinitionException.php b/src/Symfony/Components/Validator/Exception/ConstraintDefinitionException.php new file mode 100644 index 000000000000..6cc2a3546fe7 --- /dev/null +++ b/src/Symfony/Components/Validator/Exception/ConstraintDefinitionException.php @@ -0,0 +1,7 @@ +options = $options; + } + + public function getOptions() + { + return $this->options; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Exception/MappingException.php b/src/Symfony/Components/Validator/Exception/MappingException.php new file mode 100644 index 000000000000..86fd7f7f70e9 --- /dev/null +++ b/src/Symfony/Components/Validator/Exception/MappingException.php @@ -0,0 +1,5 @@ +options = $options; + } + + public function getOptions() + { + return $this->options; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Exception/UnexpectedTypeException.php b/src/Symfony/Components/Validator/Exception/UnexpectedTypeException.php new file mode 100644 index 000000000000..04c90e99be44 --- /dev/null +++ b/src/Symfony/Components/Validator/Exception/UnexpectedTypeException.php @@ -0,0 +1,11 @@ + + * @author Bernhard Schussek + */ +class DependencyInjectionValidatorFactory implements ConstraintValidatorFactoryInterface +{ + + protected $container; + + /** + * @param ContainerInterface $container + */ + public function __construct(ContainerInterface $container) + { + $this->container = $container; + } + + /** + * Returns a contraint validator from the container service, setting it if it + * doesn't exist yet + * + * Throws an exception if validator service is not instance of + * ConstraintValidatorInterface. + * + * @param Constraint $constraint + * @return ConstraintValidatorInterface + * @throws \LogicException + */ + public function getInstance(Constraint $constraint) + { + $className = $constraint->validatedBy(); + $id = $this->getServiceIdFromClass($className); + + if (!$this->container->hasService($id)) + { + $this->container->setService($id, new $className()); + } + + $validator = $this->container->getService($id); + + if (!$validator instanceof ConstraintValidatorInterface) + { + throw new \LogicException('Service "' . $id . '" is not instance of ConstraintValidatorInterface'); + } + + return $validator; + } + + /** + * Returns the matching service ID for the given validator class name + * + * @param string $className + * @return string + */ + protected function getServiceIdFromClass($className) + { + return str_replace('\\', '.', $className); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/GraphWalker.php b/src/Symfony/Components/Validator/GraphWalker.php new file mode 100644 index 000000000000..e5e78d1fcae4 --- /dev/null +++ b/src/Symfony/Components/Validator/GraphWalker.php @@ -0,0 +1,97 @@ +context = new ValidationContext($root, $this, $metadataFactory, $messageInterpolator); + $this->validatorFactory = $factory; + $this->metadataFactory = $metadataFactory; + } + + public function getViolations() + { + return $this->context->getViolations(); + } + + public function walkClass(ClassMetadata $metadata, $object, $group, $propertyPath) + { + $this->context->setCurrentClass($metadata->getClassName()); + + foreach ($metadata->findConstraints($group) as $constraint) + { + $this->walkConstraint($constraint, $object, $group, $propertyPath); + } + + if ($object !== null) + { + foreach ($metadata->getConstrainedProperties() as $property) + { + $localPropertyPath = empty($propertyPath) ? $property : $propertyPath.'.'.$property; + + $this->walkProperty($metadata, $property, $object, $group, $localPropertyPath); + } + } + } + + public function walkProperty(ClassMetadata $metadata, $property, $object, $group, $propertyPath) + { + foreach ($metadata->getMemberMetadatas($property) as $member) + { + $this->walkMember($member, $member->getValue($object), $group, $propertyPath); + } + } + + public function walkPropertyValue(ClassMetadata $metadata, $property, $value, $group, $propertyPath) + { + foreach ($metadata->getMemberMetadatas($property) as $member) + { + $this->walkMember($member, $value, $group, $propertyPath); + } + } + + protected function walkMember(MemberMetadata $metadata, $value, $group, $propertyPath) + { + $this->context->setCurrentProperty($metadata->getPropertyName()); + + foreach ($metadata->findConstraints($group) as $constraint) + { + $this->walkConstraint($constraint, $value, $group, $propertyPath); + } + } + + public function walkConstraint(Constraint $constraint, $value, $group, $propertyPath) + { + $validator = $this->validatorFactory->getInstance($constraint); + + $this->context->setPropertyPath($propertyPath); + $this->context->setGroup($group); + + $validator->initialize($this->context); + + if (!$validator->isValid($value, $constraint)) + { + $this->context->addViolation( + $validator->getMessageTemplate(), + $validator->getMessageParameters(), + $value + ); + } + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/GroupChain.php b/src/Symfony/Components/Validator/GroupChain.php new file mode 100644 index 000000000000..be61163fc42d --- /dev/null +++ b/src/Symfony/Components/Validator/GroupChain.php @@ -0,0 +1,39 @@ +groups[$group] = $group; + } + + public function addGroupSequence(array $groups) + { + if (count($groups) == 0) + { + throw new \InvalidArgumentException('A group sequence must contain at least one group'); + } + + if (!in_array($groups, $this->groupSequences, true)) + { + $this->groupSequences[] = $groups; + } + } + + public function getGroups() + { + return $this->groups; + } + + public function getGroupSequences() + { + return $this->groupSequences; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/ClassMetadata.php b/src/Symfony/Components/Validator/Mapping/ClassMetadata.php new file mode 100644 index 000000000000..d2554dca5e0c --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/ClassMetadata.php @@ -0,0 +1,233 @@ +name = $class; + $this->shortName = substr($class, strrpos($class, '\\') + 1); + } + + /** + * Returns the fully qualified name of the class + * + * @return string The fully qualified class name + */ + public function getClassName() + { + return $this->name; + } + + /** + * Returns the class name without namespace + * + * @return string The local class name in the namespace + */ + public function getShortClassName() + { + return $this->shortName; + } + + /** + * {@inheritDoc} + */ + public function addConstraint(Constraint $constraint) + { + $constraint->addImplicitGroupName($this->getShortClassName()); + + parent::addConstraint($constraint); + } + + /** + * Adds a constraint to the given property + * + * @param string $property The name of the property + * @param Constraint $constraint The constraint + * @return ClassMetadata This object + */ + public function addPropertyConstraint($property, Constraint $constraint) + { + if (!isset($this->properties[$property])) + { + $this->properties[$property] = new PropertyMetadata($this->getClassName(), $property); + + $this->addMemberMetadata($this->properties[$property]); + } + + $constraint->addImplicitGroupName($this->getShortClassName()); + + $this->properties[$property]->addConstraint($constraint); + + return $this; + } + + /** + * Adds a constraint to the getter of the given property + * + * The name of the getter is assumed to be the name of the property with an + * uppercased first letter and either the prefix "get" or "is". + * + * @param string $property The name of the property + * @param Constraint $constraint The constraint + * @return ClassMetadata This object + */ + public function addGetterConstraint($property, Constraint $constraint) + { + if (!isset($this->getters[$property])) + { + $this->getters[$property] = new GetterMetadata($this->getClassName(), $property); + + $this->addMemberMetadata($this->getters[$property]); + } + + $constraint->addImplicitGroupName($this->getShortClassName()); + + $this->getters[$property]->addConstraint($constraint); + + return $this; + } + + /** + * Merges the constraints of the given metadata into this object + * + * @param ClassMetadata $source The source metadata + */ + public function mergeConstraints(ClassMetadata $source) + { + foreach ($source->getConstraints() as $constraint) + { + $this->addConstraint(clone $constraint); + } + + foreach ($source->getConstrainedProperties() as $property) + { + foreach ($source->getMemberMetadatas($property) as $member) + { + $member = clone $member; + + foreach ($member->getConstraints() as $constraint) + { + $constraint->addImplicitGroupName($this->getShortClassName()); + } + + $this->addMemberMetadata($member); + + if (!$member->isPrivate()) + { + $property = $member->getPropertyName(); + + if ($member instanceof PropertyMetadata && !isset($this->properties[$property])) + { + $this->properties[$property] = $member; + } + else if ($member instanceof GetterMetadata && !isset($this->getters[$property])) + { + $this->getters[$property] = $member; + } + } + } + } + } + + /** + * Adds a member metadata + * + * @param MemberMetadata $metadata + */ + protected function addMemberMetadata(MemberMetadata $metadata) + { + $property = $metadata->getPropertyName(); + + if (!isset($this->members[$property])) + { + $this->members[$property] = array(); + } + + $this->members[$property][] = $metadata; + } + + /** + * Returns all metadatas of members describing the given property + * + * @param string $property The name of the property + */ + public function getMemberMetadatas($property) + { + return $this->members[$property]; + } + + /** + * Returns all properties for which constraints are defined + * + * @return array An array of property names + */ + public function getConstrainedProperties() + { + return array_keys($this->members); + } + + /** + * Sets the default group sequence for this class + * + * @param array $groups An array of group names + */ + public function setGroupSequence(array $groups) + { + $this->groupSequence = $groups; + + return $this; + } + + /** + * Returns whether this class has an overridden default group sequence + * + * @return boolean + */ + public function hasGroupSequence() + { + return count($this->groupSequence) > 0; + } + + /** + * Returns the default group sequence for this class + * + * @return array An array of group names + */ + public function getGroupSequence() + { + return $this->groupSequence; + } + + /** + * Returns a ReflectionClass instance for this class + * + * @return ReflectionClass + */ + public function getReflectionClass() + { + if (!$this->reflClass) + { + $this->reflClass = new \ReflectionClass($this->getClassName()); + } + + return $this->reflClass; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/ClassMetadataFactory.php b/src/Symfony/Components/Validator/Mapping/ClassMetadataFactory.php new file mode 100644 index 000000000000..dce68ec90f1e --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/ClassMetadataFactory.php @@ -0,0 +1,45 @@ +loader = $loader; + } + + public function getClassMetadata($class) + { + $class = ltrim($class, '\\'); + + if (!isset($this->loadedClasses[$class])) + { + $metadata = new ClassMetadata($class); + + // Include constraints from the parent class + if ($parent = $metadata->getReflectionClass()->getParentClass()) + { + $metadata->mergeConstraints($this->getClassMetadata($parent->getName())); + } + + // Include constraints from all implemented interfaces + foreach ($metadata->getReflectionClass()->getInterfaces() as $interface) + { + $metadata->mergeConstraints($this->getClassMetadata($interface->getName())); + } + + $this->loader->loadClassMetadata($metadata); + + $this->loadedClasses[$class] = $metadata; + } + + return $this->loadedClasses[$class]; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/ClassMetadataFactoryInterface.php b/src/Symfony/Components/Validator/Mapping/ClassMetadataFactoryInterface.php new file mode 100644 index 000000000000..8ed5934e32bb --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/ClassMetadataFactoryInterface.php @@ -0,0 +1,8 @@ +constraints; + + $this->constraints = array(); + $this->constraintsByGroup = array(); + + foreach ($constraints as $constraint) + { + $this->addConstraint(clone $constraint); + } + } + + /** + * Adds a constraint to this element + * + * @param Constraint $constraint + */ + public function addConstraint(Constraint $constraint) + { + $this->constraints[] = $constraint; + + foreach ($constraint->groups as $group) + { + if (!isset($this->constraintsByGroup[$group])) + { + $this->constraintsByGroup[$group] = array(); + } + + $this->constraintsByGroup[$group][] = $constraint; + } + + return $this; + } + + /** + * Returns all constraints of this element + * + * @return array An array of Constraint instances + */ + public function getConstraints() + { + return $this->constraints; + } + + /** + * Returns whether this element has any constraints + * + * @return boolean + */ + public function hasConstraints() + { + return count($this->constraints) > 0; + } + + /** + * Returns the constraints of the given group + * + * @param string $group The group name + * @return array An array with all Constraint instances belonging to + * the group + */ + public function findConstraints($group) + { + return isset($this->constraintsByGroup[$group]) + ? $this->constraintsByGroup[$group] + : array(); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/GetterMetadata.php b/src/Symfony/Components/Validator/Mapping/GetterMetadata.php new file mode 100644 index 000000000000..ec11449358c2 --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/GetterMetadata.php @@ -0,0 +1,51 @@ +getReflectionMember()->invoke($object); + } + + /** + * {@inheritDoc} + */ + protected function newReflectionMember() + { + return new \ReflectionMethod($this->getClassName(), $this->getName()); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/Loader/AnnotationLoader.php b/src/Symfony/Components/Validator/Mapping/Loader/AnnotationLoader.php new file mode 100644 index 000000000000..d0eb7b8d89c0 --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/Loader/AnnotationLoader.php @@ -0,0 +1,70 @@ +reader = new AnnotationReader(); + $this->reader->setDefaultAnnotationNamespace('Symfony\Components\Validator\Constraints\\'); + $this->reader->setAutoloadAnnotations(true); + } + + /** + * {@inheritDoc} + */ + public function loadClassMetadata(ClassMetadata $metadata) + { + $annotClass = 'Symfony\Components\Validator\Constraints\Validation'; + $reflClass = $metadata->getReflectionClass(); + $loaded = false; + + if ($annot = $this->reader->getClassAnnotation($reflClass, $annotClass)) + { + foreach ($annot->constraints as $constraint) + { + $metadata->addConstraint($constraint); + } + + $loaded = true; + } + + foreach ($reflClass->getProperties() as $property) + { + if ($annot = $this->reader->getPropertyAnnotation($property, $annotClass)) + { + foreach ($annot->constraints as $constraint) + { + $metadata->addPropertyConstraint($property->getName(), $constraint); + } + + $loaded = true; + } + } + + foreach ($reflClass->getMethods() as $method) + { + if ($annot = $this->reader->getMethodAnnotation($method, $annotClass)) + { + foreach ($annot->constraints as $constraint) + { + // TODO: clean this up + $name = lcfirst(substr($method->getName(), 0, 3)=='get' ? substr($method->getName(), 3) : substr($method->getName(), 2)); + + $metadata->addGetterConstraint($name, $constraint); + } + + $loaded = true; + } + } + + return $loaded; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/Loader/FileLoader.php b/src/Symfony/Components/Validator/Mapping/Loader/FileLoader.php new file mode 100644 index 000000000000..7e8278f01d5e --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/Loader/FileLoader.php @@ -0,0 +1,27 @@ +file = $file; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/Loader/FilesLoader.php b/src/Symfony/Components/Validator/Mapping/Loader/FilesLoader.php new file mode 100644 index 000000000000..7a6b865a2c5e --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/Loader/FilesLoader.php @@ -0,0 +1,45 @@ + + */ +abstract class FilesLoader extends LoaderChain +{ + /** + * Array of mapping files + * @param array $paths + */ + public function __construct(array $paths) + { + parent::__construct($this->getFileLoaders($paths)); + } + + /** + * Array of mapping files + * @param array $paths + * @return array - array of metadata loaders + */ + protected function getFileLoaders($paths) + { + $loaders = array(); + foreach ($paths as $path) { + $loaders[] = $this->getFileLoaderInstance($path); + } + return $loaders; + } + + /** + * Takes mapping file path + * @param string $file + * @return Symfony\Components\Validator\Mapping\Loader\LoaderInterface + */ + abstract protected function getFileLoaderInstance($file); +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/Loader/LoaderChain.php b/src/Symfony/Components/Validator/Mapping/Loader/LoaderChain.php new file mode 100644 index 000000000000..5c14f01926fc --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/Loader/LoaderChain.php @@ -0,0 +1,56 @@ +all of these loaders, regardless of whether any of them was + * successful or not. + * + * @author Bernhard Schussek + */ +class LoaderChain implements LoaderInterface +{ + protected $loaders; + + /** + * Acccepts a list of LoaderInterface instances + * + * @param array $loaders An array of LoaderInterface instances + * @throws MappingException If any of the loaders does not implement + * LoaderInterface + */ + public function __construct(array $loaders) + { + foreach ($loaders as $loader) + { + if (!$loader instanceof LoaderInterface) + { + throw new MappingException(sprintf('Class %s is expected to implement LoaderInterface', get_class($loader))); + } + } + + $this->loaders = $loaders; + } + + /** + * {@inheritDoc} + */ + public function loadClassMetadata(ClassMetadata $metadata) + { + $success = false; + + foreach ($this->loaders as $loader) + { + $success = $loader->loadClassMetadata($metadata) || $success; + } + + return $success; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/Loader/LoaderInterface.php b/src/Symfony/Components/Validator/Mapping/Loader/LoaderInterface.php new file mode 100644 index 000000000000..f28a03b4b5ae --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/Loader/LoaderInterface.php @@ -0,0 +1,14 @@ +methodName = $methodName; + } + + /** + * {@inheritDoc} + */ + public function loadClassMetadata(ClassMetadata $metadata) + { + $reflClass = $metadata->getReflectionClass(); + + if ($reflClass->hasMethod($this->methodName)) + { + $reflMethod = $reflClass->getMethod($this->methodName); + + if (!$reflMethod->isStatic()) + { + throw new MappingException(sprintf('The method %s::%s should be static', $reflClass->getName(), $this->methodName)); + } + + $reflMethod->invoke(null, $metadata); + + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/Loader/XmlFileLoader.php b/src/Symfony/Components/Validator/Mapping/Loader/XmlFileLoader.php new file mode 100644 index 000000000000..d533381578f4 --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/Loader/XmlFileLoader.php @@ -0,0 +1,237 @@ +classes)) + { + $this->classes = array(); + $xml = $this->parseFile($this->file); + + foreach ($xml->class as $class) + { + $this->classes[(string)$class['name']] = $class; + } + } + + if (isset($this->classes[$metadata->getClassName()])) + { + $xml = $this->classes[$metadata->getClassName()]; + + foreach ($this->parseConstraints($xml->constraint) as $constraint) + { + $metadata->addConstraint($constraint); + } + + foreach ($xml->property as $property) + { + foreach ($this->parseConstraints($property->constraint) as $constraint) + { + $metadata->addPropertyConstraint((string)$property['name'], $constraint); + } + } + + foreach ($xml->getter as $getter) + { + foreach ($this->parseConstraints($getter->constraint) as $constraint) + { + $metadata->addGetterConstraint((string)$getter['property'], $constraint); + } + } + + return true; + } + + return false; + } + + /** + * Parses a collection of "constraint" XML nodes + * + * @param SimpleXMLElement $nodes The XML nodes + * @return array The Constraint instances + */ + protected function parseConstraints(\SimpleXMLElement $nodes) + { + $constraints = array(); + + foreach ($nodes as $node) + { + $className = 'Symfony\\Components\\Validator\\Constraints\\'.$node['name']; + + if (count($node) > 0) + { + if (count($node->value) > 0) + { + $options = $this->parseValues($node->value); + } + else if (count($node->constraint) > 0) + { + $options = $this->parseConstraints($node->constraint); + } + else if (count($node->option) > 0) + { + $options = $this->parseOptions($node->option); + } + else + { + $options = array(); + } + } + else if (strlen((string)$node) > 0) + { + $options = trim($node); + } + else + { + $options = null; + } + + $constraints[] = new $className($options); + } + + return $constraints; + } + + /** + * Parses a collection of "value" XML nodes + * + * @param SimpleXMLElement $nodes The XML nodes + * @return array The values + */ + protected function parseValues(\SimpleXMLElement $nodes) + { + $values = array(); + + foreach ($nodes as $node) + { + if (count($node) > 0) + { + if (count($node->value) > 0) + { + $value = $this->parseValues($node->value); + } + else if (count($node->constraint) > 0) + { + $value = $this->parseConstraints($node->constraint); + } + else + { + $value = array(); + } + } + else + { + $value = trim($node); + } + + if (isset($node['key'])) + { + $values[(string)$node['key']] = $value; + } + else + { + $values[] = $value; + } + } + + return $values; + } + + /** + * Parses a collection of "option" XML nodes + * + * @param SimpleXMLElement $nodes The XML nodes + * @return array The options + */ + protected function parseOptions(\SimpleXMLElement $nodes) + { + $options = array(); + + foreach ($nodes as $node) + { + if (count($node) > 0) + { + if (count($node->value) > 0) + { + $value = $this->parseValues($node->value); + } + else if (count($node->constraint) > 0) + { + $value = $this->parseConstraints($node->constraint); + } + else + { + $value = array(); + } + } + else + { + $value = trim($node); + } + + $options[(string)$node['name']] = $value; + } + + return $options; + } + + /** + * @param string $file + * @return SimpleXMLElement + */ + protected function parseFile($file) + { + $dom = new \DOMDocument(); + libxml_use_internal_errors(true); + if (!$dom->load($file, LIBXML_COMPACT)) + { + throw new MappingException(implode("\n", $this->getXmlErrors())); + } + if (!$dom->schemaValidate(__DIR__.'/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd')) + { + throw new MappingException(implode("\n", $this->getXmlErrors())); + } + $dom->validateOnParse = true; + $dom->normalizeDocument(); + libxml_use_internal_errors(false); + + return simplexml_import_dom($dom); + } + + protected function getXmlErrors() + { + $errors = array(); + foreach (libxml_get_errors() as $error) + { + $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)', + LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', + $error->code, + trim($error->message), + $error->file ? $error->file : 'n/a', + $error->line, + $error->column + ); + } + + libxml_clear_errors(); + libxml_use_internal_errors(false); + + return $errors; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/Loader/XmlFilesLoader.php b/src/Symfony/Components/Validator/Mapping/Loader/XmlFilesLoader.php new file mode 100644 index 000000000000..c641635558fc --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/Loader/XmlFilesLoader.php @@ -0,0 +1,19 @@ + + */ +class XmlFilesLoader extends FilesLoader +{ + /** + * {@inheritDoc} + */ + public function getFileLoaderInstance($file) + { + return new XmlFileLoader($file); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/Loader/YamlFileLoader.php b/src/Symfony/Components/Validator/Mapping/Loader/YamlFileLoader.php new file mode 100644 index 000000000000..16cdf9554592 --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/Loader/YamlFileLoader.php @@ -0,0 +1,106 @@ +classes)) + { + $this->classes = Yaml::load($this->file); + } + + // TODO validation + + if (isset($this->classes[$metadata->getClassName()])) + { + $yaml = $this->classes[$metadata->getClassName()]; + + if (isset($yaml['constraints'])) + { + foreach ($this->parseNodes($yaml['constraints']) as $constraint) + { + $metadata->addConstraint($constraint); + } + } + + if (isset($yaml['properties'])) + { + foreach ($yaml['properties'] as $property => $constraints) + { + foreach ($this->parseNodes($constraints) as $constraint) + { + $metadata->addPropertyConstraint($property, $constraint); + } + } + } + + if (isset($yaml['getters'])) + { + foreach ($yaml['getters'] as $getter => $constraints) + { + foreach ($this->parseNodes($constraints) as $constraint) + { + $metadata->addGetterConstraint($getter, $constraint); + } + } + } + + return true; + } + + return false; + } + + /** + * Parses a collection of YAML nodes + * + * @param array $nodes The YAML nodes + * @return array An array of values or Constraint instances + */ + protected function parseNodes(array $nodes) + { + $values = array(); + + foreach ($nodes as $name => $childNodes) + { + if (is_numeric($name) && is_array($childNodes) && count($childNodes) == 1) + { + $className = 'Symfony\\Components\\Validator\\Constraints\\'.key($childNodes); + $options = current($childNodes); + + if (is_array($options)) + { + $options = $this->parseNodes($options); + } + + $values[] = new $className($options); + } + else + { + if (is_array($childNodes)) + { + $childNodes = $this->parseNodes($childNodes); + } + + $values[$name] = $childNodes; + } + } + + return $values; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/Loader/YamlFilesLoader.php b/src/Symfony/Components/Validator/Mapping/Loader/YamlFilesLoader.php new file mode 100644 index 000000000000..2571fea773ee --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/Loader/YamlFilesLoader.php @@ -0,0 +1,19 @@ + + */ +class YamlFilesLoader extends FilesLoader +{ + /** + * {@inheritDoc} + */ + public function getFileLoaderInstance($file) + { + return new YamlFileLoader($file); + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd b/src/Symfony/Components/Validator/Mapping/Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd new file mode 100644 index 000000000000..ec9dd767ff1c --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/Loader/schema/dic/constraint-mapping/constraint-mapping-1.0.xsd @@ -0,0 +1,124 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/MemberMetadata.php b/src/Symfony/Components/Validator/Mapping/MemberMetadata.php new file mode 100644 index 000000000000..fe88308850b4 --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/MemberMetadata.php @@ -0,0 +1,117 @@ +class = $class; + $this->name = $name; + $this->property = $property; + } + + /** + * Returns the name of the member + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Returns the class this member is defined on + * + * @return string + */ + public function getClassName() + { + return $this->class; + } + + /** + * Returns the name of the property this member belongs to + * + * @return string The property name + */ + public function getPropertyName() + { + return $this->property; + } + + /** + * Returns whether this member is public + * + * @return boolean + */ + public function isPublic() + { + return $this->getReflectionMember()->isPublic(); + } + + /** + * Returns whether this member is protected + * + * @return boolean + */ + public function isProtected() + { + return $this->getReflectionMember()->isProtected(); + } + + /** + * Returns whether this member is private + * + * @return boolean + */ + public function isPrivate() + { + return $this->getReflectionMember()->isPrivate(); + } + + /** + * Returns the value of this property in the given object + * + * @param object $object The object + * @return mixed The property value + */ + abstract public function getValue($object); + + /** + * Returns the Reflection instance of the member + * + * @return object + */ + public function getReflectionMember() + { + if (!$this->reflMember) + { + $this->reflMember = $this->newReflectionMember(); + } + + return $this->reflMember; + } + + /** + * Creates a new Reflection instance for the member + * + * @return object + */ + abstract protected function newReflectionMember(); +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Mapping/PropertyMetadata.php b/src/Symfony/Components/Validator/Mapping/PropertyMetadata.php new file mode 100644 index 000000000000..e3bd83f43a2c --- /dev/null +++ b/src/Symfony/Components/Validator/Mapping/PropertyMetadata.php @@ -0,0 +1,43 @@ +getReflectionMember()->getValue($object); + } + + /** + * {@inheritDoc} + */ + protected function newReflectionMember() + { + $member = new \ReflectionProperty($this->getClassName(), $this->getName()); + $member->setAccessible(true); + + return $member; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/MessageInterpolator/MessageInterpolatorInterface.php b/src/Symfony/Components/Validator/MessageInterpolator/MessageInterpolatorInterface.php new file mode 100644 index 000000000000..94025a563e98 --- /dev/null +++ b/src/Symfony/Components/Validator/MessageInterpolator/MessageInterpolatorInterface.php @@ -0,0 +1,15 @@ +parseFile($file); + $xml->registerXPathNamespace('xliff', 'urn:oasis:names:tc:xliff:document:1.2'); + + foreach ($xml->xpath('//xliff:trans-unit') as $translation) + { + $this->translations[(string)$translation->source] = (string)$translation->target; + } + } + } + + /** + * {@inheritDoc} + */ + public function interpolate($text, array $parameters = array()) + { + if (isset($this->translations[$text])) + { + $text = $this->translations[$text]; + } + + $sources = array(); + $targets = array(); + + foreach ($parameters as $key => $value) + { + $sources[] = '%'.$key.'%'; + $targets[] = (string)$value; + } + + return str_replace($sources, $targets, $text); + } + + /** + * Validates and parses the given file into a SimpleXMLElement + * + * @param string $file + * @return SimpleXMLElement + */ + protected function parseFile($file) + { + $dom = new \DOMDocument(); + libxml_use_internal_errors(true); + if (!$dom->load($file, LIBXML_COMPACT)) + { + throw new \Exception(implode("\n", $this->getXmlErrors())); + } + if (!$dom->schemaValidate(__DIR__.'/schema/dic/xliff-core/xliff-core-1.2-strict.xsd')) + { + throw new \Exception(implode("\n", $this->getXmlErrors())); + } + $dom->validateOnParse = true; + $dom->normalizeDocument(); + libxml_use_internal_errors(false); + + return simplexml_import_dom($dom); + } + + /** + * Returns the XML errors of the internal XML parser + * + * @return array An array of errors + */ + protected function getXmlErrors() + { + $errors = array(); + foreach (libxml_get_errors() as $error) + { + $errors[] = sprintf('[%s %s] %s (in %s - line %d, column %d)', + LIBXML_ERR_WARNING == $error->level ? 'WARNING' : 'ERROR', + $error->code, + trim($error->message), + $error->file ? $error->file : 'n/a', + $error->line, + $error->column + ); + } + + libxml_clear_errors(); + libxml_use_internal_errors(false); + + return $errors; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/MessageInterpolator/schema/dic/xliff-core/xliff-core-1.2-strict.xsd b/src/Symfony/Components/Validator/MessageInterpolator/schema/dic/xliff-core/xliff-core-1.2-strict.xsd new file mode 100644 index 000000000000..803eb602def5 --- /dev/null +++ b/src/Symfony/Components/Validator/MessageInterpolator/schema/dic/xliff-core/xliff-core-1.2-strict.xsd @@ -0,0 +1,2223 @@ + + + + + + + + + + + + + + + Values for the attribute 'context-type'. + + + + + Indicates a database content. + + + + + Indicates the content of an element within an XML document. + + + + + Indicates the name of an element within an XML document. + + + + + Indicates the line number from the sourcefile (see context-type="sourcefile") where the <source> is found. + + + + + Indicates a the number of parameters contained within the <source>. + + + + + Indicates notes pertaining to the parameters in the <source>. + + + + + Indicates the content of a record within a database. + + + + + Indicates the name of a record within a database. + + + + + Indicates the original source file in the case that multiple files are merged to form the original file from which the XLIFF file is created. This differs from the original <file> attribute in that this sourcefile is one of many that make up that file. + + + + + + + Values for the attribute 'count-type'. + + + + + Indicates the count units are items that are used X times in a certain context; example: this is a reusable text unit which is used 42 times in other texts. + + + + + Indicates the count units are translation units existing already in the same document. + + + + + Indicates a total count. + + + + + + + Values for the attribute 'ctype' when used other elements than <ph> or <x>. + + + + + Indicates a run of bolded text. + + + + + Indicates a run of text in italics. + + + + + Indicates a run of underlined text. + + + + + Indicates a run of hyper-text. + + + + + + + Values for the attribute 'ctype' when used with <ph> or <x>. + + + + + Indicates a inline image. + + + + + Indicates a page break. + + + + + Indicates a line break. + + + + + + + + + + + + Values for the attribute 'datatype'. + + + + + Indicates Active Server Page data. + + + + + Indicates C source file data. + + + + + Indicates Channel Definition Format (CDF) data. + + + + + Indicates ColdFusion data. + + + + + Indicates C++ source file data. + + + + + Indicates C-Sharp data. + + + + + Indicates strings from C, ASM, and driver files data. + + + + + Indicates comma-separated values data. + + + + + Indicates database data. + + + + + Indicates portions of document that follows data and contains metadata. + + + + + Indicates portions of document that precedes data and contains metadata. + + + + + Indicates data from standard UI file operations dialogs (e.g., Open, Save, Save As, Export, Import). + + + + + Indicates standard user input screen data. + + + + + Indicates HyperText Markup Language (HTML) data - document instance. + + + + + Indicates content within an HTML document’s <body> element. + + + + + Indicates Windows INI file data. + + + + + Indicates Interleaf data. + + + + + Indicates Java source file data (extension '.java'). + + + + + Indicates Java property resource bundle data. + + + + + Indicates Java list resource bundle data. + + + + + Indicates JavaScript source file data. + + + + + Indicates JScript source file data. + + + + + Indicates information relating to formatting. + + + + + Indicates LISP source file data. + + + + + Indicates information relating to margin formats. + + + + + Indicates a file containing menu. + + + + + Indicates numerically identified string table. + + + + + Indicates Maker Interchange Format (MIF) data. + + + + + Indicates that the datatype attribute value is a MIME Type value and is defined in the mime-type attribute. + + + + + Indicates GNU Machine Object data. + + + + + Indicates Message Librarian strings created by Novell's Message Librarian Tool. + + + + + Indicates information to be displayed at the bottom of each page of a document. + + + + + Indicates information to be displayed at the top of each page of a document. + + + + + Indicates a list of property values (e.g., settings within INI files or preferences dialog). + + + + + Indicates Pascal source file data. + + + + + Indicates Hypertext Preprocessor data. + + + + + Indicates plain text file (no formatting other than, possibly, wrapping). + + + + + Indicates GNU Portable Object file. + + + + + Indicates dynamically generated user defined document. e.g. Oracle Report, Crystal Report, etc. + + + + + Indicates Windows .NET binary resources. + + + + + Indicates Windows .NET Resources. + + + + + Indicates Rich Text Format (RTF) data. + + + + + Indicates Standard Generalized Markup Language (SGML) data - document instance. + + + + + Indicates Standard Generalized Markup Language (SGML) data - Document Type Definition (DTD). + + + + + Indicates Scalable Vector Graphic (SVG) data. + + + + + Indicates VisualBasic Script source file. + + + + + Indicates warning message. + + + + + Indicates Windows (Win32) resources (i.e. resources extracted from an RC script, a message file, or a compiled file). + + + + + Indicates Extensible HyperText Markup Language (XHTML) data - document instance. + + + + + Indicates Extensible Markup Language (XML) data - document instance. + + + + + Indicates Extensible Markup Language (XML) data - Document Type Definition (DTD). + + + + + Indicates Extensible Stylesheet Language (XSL) data. + + + + + Indicates XUL elements. + + + + + + + Values for the attribute 'mtype'. + + + + + Indicates the marked text is an abbreviation. + + + + + ISO-12620 2.1.8: A term resulting from the omission of any part of the full term while designating the same concept. + + + + + ISO-12620 2.1.8.1: An abbreviated form of a simple term resulting from the omission of some of its letters (e.g. 'adj.' for 'adjective'). + + + + + ISO-12620 2.1.8.4: An abbreviated form of a term made up of letters from the full form of a multiword term strung together into a sequence pronounced only syllabically (e.g. 'radar' for 'radio detecting and ranging'). + + + + + ISO-12620: A proper-name term, such as the name of an agency or other proper entity. + + + + + ISO-12620 2.1.18.1: A recurrent word combination characterized by cohesion in that the components of the collocation must co-occur within an utterance or series of utterances, even though they do not necessarily have to maintain immediate proximity to one another. + + + + + ISO-12620 2.1.5: A synonym for an international scientific term that is used in general discourse in a given language. + + + + + Indicates the marked text is a date and/or time. + + + + + ISO-12620 2.1.15: An expression used to represent a concept based on a statement that two mathematical expressions are, for instance, equal as identified by the equal sign (=), or assigned to one another by a similar sign. + + + + + ISO-12620 2.1.7: The complete representation of a term for which there is an abbreviated form. + + + + + ISO-12620 2.1.14: Figures, symbols or the like used to express a concept briefly, such as a mathematical or chemical formula. + + + + + ISO-12620 2.1.1: The concept designation that has been chosen to head a terminological record. + + + + + ISO-12620 2.1.8.3: An abbreviated form of a term consisting of some of the initial letters of the words making up a multiword term or the term elements making up a compound term when these letters are pronounced individually (e.g. 'BSE' for 'bovine spongiform encephalopathy'). + + + + + ISO-12620 2.1.4: A term that is part of an international scientific nomenclature as adopted by an appropriate scientific body. + + + + + ISO-12620 2.1.6: A term that has the same or nearly identical orthographic or phonemic form in many languages. + + + + + ISO-12620 2.1.16: An expression used to represent a concept based on mathematical or logical relations, such as statements of inequality, set relationships, Boolean operations, and the like. + + + + + ISO-12620 2.1.17: A unit to track object. + + + + + Indicates the marked text is a name. + + + + + ISO-12620 2.1.3: A term that represents the same or a very similar concept as another term in the same language, but for which interchangeability is limited to some contexts and inapplicable in others. + + + + + ISO-12620 2.1.17.2: A unique alphanumeric designation assigned to an object in a manufacturing system. + + + + + Indicates the marked text is a phrase. + + + + + ISO-12620 2.1.18: Any group of two or more words that form a unit, the meaning of which frequently cannot be deduced based on the combined sense of the words making up the phrase. + + + + + Indicates the marked text should not be translated. + + + + + ISO-12620 2.1.12: A form of a term resulting from an operation whereby non-Latin writing systems are converted to the Latin alphabet. + + + + + Indicates that the marked text represents a segment. + + + + + ISO-12620 2.1.18.2: A fixed, lexicalized phrase. + + + + + ISO-12620 2.1.8.2: A variant of a multiword term that includes fewer words than the full form of the term (e.g. 'Group of Twenty-four' for 'Intergovernmental Group of Twenty-four on International Monetary Affairs'). + + + + + ISO-12620 2.1.17.1: Stock keeping unit, an inventory item identified by a unique alphanumeric designation assigned to an object in an inventory control system. + + + + + ISO-12620 2.1.19: A fixed chunk of recurring text. + + + + + ISO-12620 2.1.13: A designation of a concept by letters, numerals, pictograms or any combination thereof. + + + + + ISO-12620 2.1.2: Any term that represents the same or a very similar concept as the main entry term in a term entry. + + + + + ISO-12620 2.1.18.3: Phraseological unit in a language that expresses the same semantic content as another phrase in that same language. + + + + + Indicates the marked text is a term. + + + + + ISO-12620 2.1.11: A form of a term resulting from an operation whereby the characters of one writing system are represented by characters from another writing system, taking into account the pronunciation of the characters converted. + + + + + ISO-12620 2.1.10: A form of a term resulting from an operation whereby the characters of an alphabetic writing system are represented by characters from another alphabetic writing system. + + + + + ISO-12620 2.1.8.5: An abbreviated form of a term resulting from the omission of one or more term elements or syllables (e.g. 'flu' for 'influenza'). + + + + + ISO-12620 2.1.9: One of the alternate forms of a term. + + + + + + + Values for the attribute 'restype'. + + + + + Indicates a Windows RC AUTO3STATE control. + + + + + Indicates a Windows RC AUTOCHECKBOX control. + + + + + Indicates a Windows RC AUTORADIOBUTTON control. + + + + + Indicates a Windows RC BEDIT control. + + + + + Indicates a bitmap, for example a BITMAP resource in Windows. + + + + + Indicates a button object, for example a BUTTON control Windows. + + + + + Indicates a caption, such as the caption of a dialog box. + + + + + Indicates the cell in a table, for example the content of the <td> element in HTML. + + + + + Indicates check box object, for example a CHECKBOX control in Windows. + + + + + Indicates a menu item with an associated checkbox. + + + + + Indicates a list box, but with a check-box for each item. + + + + + Indicates a color selection dialog. + + + + + Indicates a combination of edit box and listbox object, for example a COMBOBOX control in Windows. + + + + + Indicates an initialization entry of an extended combobox DLGINIT resource block. (code 0x1234). + + + + + Indicates an initialization entry of a combobox DLGINIT resource block (code 0x0403). + + + + + Indicates a UI base class element that cannot be represented by any other element. + + + + + Indicates a context menu. + + + + + Indicates a Windows RC CTEXT control. + + + + + Indicates a cursor, for example a CURSOR resource in Windows. + + + + + Indicates a date/time picker. + + + + + Indicates a Windows RC DEFPUSHBUTTON control. + + + + + Indicates a dialog box. + + + + + Indicates a Windows RC DLGINIT resource block. + + + + + Indicates an edit box object, for example an EDIT control in Windows. + + + + + Indicates a filename. + + + + + Indicates a file dialog. + + + + + Indicates a footnote. + + + + + Indicates a font name. + + + + + Indicates a footer. + + + + + Indicates a frame object. + + + + + Indicates a XUL grid element. + + + + + Indicates a groupbox object, for example a GROUPBOX control in Windows. + + + + + Indicates a header item. + + + + + Indicates a heading, such has the content of <h1>, <h2>, etc. in HTML. + + + + + Indicates a Windows RC HEDIT control. + + + + + Indicates a horizontal scrollbar. + + + + + Indicates an icon, for example an ICON resource in Windows. + + + + + Indicates a Windows RC IEDIT control. + + + + + Indicates keyword list, such as the content of the Keywords meta-data in HTML, or a K footnote in WinHelp RTF. + + + + + Indicates a label object. + + + + + Indicates a label that is also a HTML link (not necessarily a URL). + + + + + Indicates a list (a group of list-items, for example an <ol> or <ul> element in HTML). + + + + + Indicates a listbox object, for example an LISTBOX control in Windows. + + + + + Indicates an list item (an entry in a list). + + + + + Indicates a Windows RC LTEXT control. + + + + + Indicates a menu (a group of menu-items). + + + + + Indicates a toolbar containing one or more tope level menus. + + + + + Indicates a menu item (an entry in a menu). + + + + + Indicates a XUL menuseparator element. + + + + + Indicates a message, for example an entry in a MESSAGETABLE resource in Windows. + + + + + Indicates a calendar control. + + + + + Indicates an edit box beside a spin control. + + + + + Indicates a catch all for rectangular areas. + + + + + Indicates a standalone menu not necessarily associated with a menubar. + + + + + Indicates a pushbox object, for example a PUSHBOX control in Windows. + + + + + Indicates a Windows RC PUSHBUTTON control. + + + + + Indicates a radio button object. + + + + + Indicates a menuitem with associated radio button. + + + + + Indicates raw data resources for an application. + + + + + Indicates a row in a table. + + + + + Indicates a Windows RC RTEXT control. + + + + + Indicates a user navigable container used to show a portion of a document. + + + + + Indicates a generic divider object (e.g. menu group separator). + + + + + Windows accelerators, shortcuts in resource or property files. + + + + + Indicates a UI control to indicate process activity but not progress. + + + + + Indicates a splitter bar. + + + + + Indicates a Windows RC STATE3 control. + + + + + Indicates a window for providing feedback to the users, like 'read-only', etc. + + + + + Indicates a string, for example an entry in a STRINGTABLE resource in Windows. + + + + + Indicates a layers of controls with a tab to select layers. + + + + + Indicates a display and edits regular two-dimensional tables of cells. + + + + + Indicates a XUL textbox element. + + + + + Indicates a UI button that can be toggled to on or off state. + + + + + Indicates an array of controls, usually buttons. + + + + + Indicates a pop up tool tip text. + + + + + Indicates a bar with a pointer indicating a position within a certain range. + + + + + Indicates a control that displays a set of hierarchical data. + + + + + Indicates a URI (URN or URL). + + + + + Indicates a Windows RC USERBUTTON control. + + + + + Indicates a user-defined control like CONTROL control in Windows. + + + + + Indicates the text of a variable. + + + + + Indicates version information about a resource like VERSIONINFO in Windows. + + + + + Indicates a vertical scrollbar. + + + + + Indicates a graphical window. + + + + + + + Values for the attribute 'size-unit'. + + + + + Indicates a size in 8-bit bytes. + + + + + Indicates a size in Unicode characters. + + + + + Indicates a size in columns. Used for HTML text area. + + + + + Indicates a size in centimeters. + + + + + Indicates a size in dialog units, as defined in Windows resources. + + + + + Indicates a size in 'font-size' units (as defined in CSS). + + + + + Indicates a size in 'x-height' units (as defined in CSS). + + + + + Indicates a size in glyphs. A glyph is considered to be one or more combined Unicode characters that represent a single displayable text character. Sometimes referred to as a 'grapheme cluster' + + + + + Indicates a size in inches. + + + + + Indicates a size in millimeters. + + + + + Indicates a size in percentage. + + + + + Indicates a size in pixels. + + + + + Indicates a size in point. + + + + + Indicates a size in rows. Used for HTML text area. + + + + + + + Values for the attribute 'state'. + + + + + Indicates the terminating state. + + + + + Indicates only non-textual information needs adaptation. + + + + + Indicates both text and non-textual information needs adaptation. + + + + + Indicates only non-textual information needs review. + + + + + Indicates both text and non-textual information needs review. + + + + + Indicates that only the text of the item needs to be reviewed. + + + + + Indicates that the item needs to be translated. + + + + + Indicates that the item is new. For example, translation units that were not in a previous version of the document. + + + + + Indicates that changes are reviewed and approved. + + + + + Indicates that the item has been translated. + + + + + + + Values for the attribute 'state-qualifier'. + + + + + Indicates an exact match. An exact match occurs when a source text of a segment is exactly the same as the source text of a segment that was translated previously. + + + + + Indicates a fuzzy match. A fuzzy match occurs when a source text of a segment is very similar to the source text of a segment that was translated previously (e.g. when the difference is casing, a few changed words, white-space discripancy, etc.). + + + + + Indicates a match based on matching IDs (in addition to matching text). + + + + + Indicates a translation derived from a glossary. + + + + + Indicates a translation derived from existing translation. + + + + + Indicates a translation derived from machine translation. + + + + + Indicates a translation derived from a translation repository. + + + + + Indicates a translation derived from a translation memory. + + + + + Indicates the translation is suggested by machine translation. + + + + + Indicates that the item has been rejected because of incorrect grammar. + + + + + Indicates that the item has been rejected because it is incorrect. + + + + + Indicates that the item has been rejected because it is too long or too short. + + + + + Indicates that the item has been rejected because of incorrect spelling. + + + + + Indicates the translation is suggested by translation memory. + + + + + + + Values for the attribute 'unit'. + + + + + Refers to words. + + + + + Refers to pages. + + + + + Refers to <trans-unit> elements. + + + + + Refers to <bin-unit> elements. + + + + + Refers to glyphs. + + + + + Refers to <trans-unit> and/or <bin-unit> elements. + + + + + Refers to the occurrences of instances defined by the count-type value. + + + + + Refers to characters. + + + + + Refers to lines. + + + + + Refers to sentences. + + + + + Refers to paragraphs. + + + + + Refers to segments. + + + + + Refers to placeables (inline elements). + + + + + + + Values for the attribute 'priority'. + + + + + Highest priority. + + + + + High priority. + + + + + High priority, but not as important as 2. + + + + + High priority, but not as important as 3. + + + + + Medium priority, but more important than 6. + + + + + Medium priority, but less important than 5. + + + + + Low priority, but more important than 8. + + + + + Low priority, but more important than 9. + + + + + Low priority. + + + + + Lowest priority. + + + + + + + + + This value indicates that all properties can be reformatted. This value must be used alone. + + + + + This value indicates that no properties should be reformatted. This value must be used alone. + + + + + + + + + + + + + This value indicates that all information in the coord attribute can be modified. + + + + + This value indicates that the x information in the coord attribute can be modified. + + + + + This value indicates that the y information in the coord attribute can be modified. + + + + + This value indicates that the cx information in the coord attribute can be modified. + + + + + This value indicates that the cy information in the coord attribute can be modified. + + + + + This value indicates that all the information in the font attribute can be modified. + + + + + This value indicates that the name information in the font attribute can be modified. + + + + + This value indicates that the size information in the font attribute can be modified. + + + + + This value indicates that the weight information in the font attribute can be modified. + + + + + This value indicates that the information in the css-style attribute can be modified. + + + + + This value indicates that the information in the style attribute can be modified. + + + + + This value indicates that the information in the exstyle attribute can be modified. + + + + + + + + + + + + + Indicates that the context is informational in nature, specifying for example, how a term should be translated. Thus, should be displayed to anyone editing the XLIFF document. + + + + + Indicates that the context-group is used to specify where the term was found in the translatable source. Thus, it is not displayed. + + + + + Indicates that the context information should be used during translation memory lookups. Thus, it is not displayed. + + + + + + + + + Represents a translation proposal from a translation memory or other resource. + + + + + Represents a previous version of the target element. + + + + + Represents a rejected version of the target element. + + + + + Represents a translation to be used for reference purposes only, for example from a related product or a different language. + + + + + Represents a proposed translation that was used for the translation of the trans-unit, possibly modified. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Values for the attribute 'coord'. + + + + + + + + Version values: 1.0 and 1.1 are allowed for backward compatibility. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Resources/i18n/messages.en.xml b/src/Symfony/Components/Validator/Resources/i18n/messages.en.xml new file mode 100644 index 000000000000..725181e28b70 --- /dev/null +++ b/src/Symfony/Components/Validator/Resources/i18n/messages.en.xml @@ -0,0 +1,115 @@ + + + + + + Symfony.Validator.AssertFalse.message + This value should be false + + + Symfony.Validator.AssertTrue.message + This value should be true + + + Symfony.Validator.AssertType.message + This value should be of type %type% + + + Symfony.Validator.Blank.message + This value should be blank + + + Symfony.Validator.Choice.message + This value should be one of the given choices + + + Symfony.Validator.Choice.minMessage + You should select at least %limit% choices + + + Symfony.Validator.Choice.maxMessage + You should select at most %limit% choices + + + Symfony.Validator.Collection.extraFieldsMessage + The fields %fields% were not expected + + + Symfony.Validator.Collection.missingFieldsMessage + The fields %fields% are missing + + + Symfony.Validator.Date.message + This value is not a valid date + + + Symfony.Validator.DateTime.message + This value is not a valid datetime + + + Symfony.Validator.Email.message + This value is not a valid email address + + + Symfony.Validator.File.notFoundMessage + The file could not be found + + + Symfony.Validator.File.notReadableMessage + The file is not readable + + + Symfony.Validator.File.maxSizeMessage + The file is too large (%size%). Allowed maximum size is %limit% + + + Symfony.Validator.File.mimeTypesMessage + The mime type of the file is invalid (%type%). Allowed mime types are %types% + + + Symfony.Validator.Max.message + This value should be %limit% or less + + + Symfony.Validator.MaxLength.message + This value is too long. It should have %limit% characters or less + + + Symfony.Validator.Min.message + This value should be %limit% or more + + + Symfony.Validator.MinLength.message + This value is too short. It should have %limit% characters or more + + + Symfony.Validator.NotBlank.message + This value should not be blank + + + Symfony.Validator.NotNull.message + This value should not be null + + + Symfony.Validator.Null.message + This value should be null + + + Symfony.Validator.Regex.message + This value is not valid + + + Symfony.Validator.Time.message + This value is not a valid time + + + Symfony.Validator.Url.message + This value is not a valid URL + + + Symfony.Validator.Valid.message + This value should be instance of class %class% + + + + \ No newline at end of file diff --git a/src/Symfony/Components/Validator/ValidationContext.php b/src/Symfony/Components/Validator/ValidationContext.php new file mode 100644 index 000000000000..259ca17687f7 --- /dev/null +++ b/src/Symfony/Components/Validator/ValidationContext.php @@ -0,0 +1,108 @@ +root = $root; + $this->graphWalker = $graphWalker; + $this->messageInterpolator = $messageInterpolator; + $this->metadataFactory = $metadataFactory; + $this->violations = new ConstraintViolationList(); + } + + public function __clone() + { + $this->violations = clone $this->violations; + } + + public function addViolation($message, array $params, $invalidValue) + { + $this->violations->add(new ConstraintViolation( + $this->messageInterpolator->interpolate($message, $params), + $this->root, + $this->propertyPath, + $invalidValue + )); + } + + public function getViolations() + { + return $this->violations; + } + + public function getRoot() + { + return $this->root; + } + + public function setPropertyPath($propertyPath) + { + $this->propertyPath = $propertyPath; + } + + public function getPropertyPath() + { + return $this->propertyPath; + } + + public function setCurrentClass($class) + { + $this->class = $class; + } + + public function getCurrentClass() + { + return $this->class; + } + + public function setCurrentProperty($property) + { + $this->property = $property; + } + + public function getCurrentProperty() + { + return $this->property; + } + + public function setGroup($group) + { + $this->group = $group; + } + + public function getGroup() + { + return $this->group; + } + + public function getGraphWalker() + { + return $this->graphWalker; + } + + public function getClassMetadataFactory() + { + return $this->metadataFactory; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/Validator.php b/src/Symfony/Components/Validator/Validator.php new file mode 100644 index 000000000000..ac6acffab1b6 --- /dev/null +++ b/src/Symfony/Components/Validator/Validator.php @@ -0,0 +1,149 @@ +metadataFactory = $metadataFactory; + $this->validatorFactory = $validatorFactory; + $this->messageInterpolator = $messageInterpolator; + } + + public function validate($object, $groups = null) + { + $metadata = $this->metadataFactory->getClassMetadata(get_class($object)); + $groupChain = $this->buildGroupChain($metadata, $groups); + + $closure = function(GraphWalker $walker, $group) use ($metadata, $object) { + return $walker->walkClass($metadata, $object, $group, ''); + }; + + return $this->validateGraph($object, $closure, $groupChain); + } + + public function validateProperty($object, $property, $groups = null) + { + $metadata = $this->metadataFactory->getClassMetadata(get_class($object)); + $groupChain = $this->buildGroupChain($metadata, $groups); + + $closure = function(GraphWalker $walker, $group) use ($metadata, $property, $object) { + return $walker->walkProperty($metadata, $property, $object, $group, ''); + }; + + return $this->validateGraph($object, $closure, $groupChain); + } + + public function validatePropertyValue($class, $property, $value, $groups = null) + { + $metadata = $this->metadataFactory->getClassMetadata($class); + $groupChain = $this->buildGroupChain($metadata, $groups); + + $closure = function(GraphWalker $walker, $group) use ($metadata, $property, $value) { + return $walker->walkPropertyValue($metadata, $property, $value, $group, ''); + }; + + return $this->validateGraph($object, $closure, $groupChain); + } + + public function validateValue($value, Constraint $constraint, $groups = null) + { + $groupChain = $this->buildSimpleGroupChain($groups); + + $closure = function(GraphWalker $walker, $group) use ($constraint, $value) { + return $walker->walkConstraint($constraint, $value, $group, ''); + }; + + return $this->validateGraph($value, $closure, $groupChain); + } + + protected function validateGraph($root, \Closure $closure, GroupChain $groupChain) + { + $walker = new GraphWalker($root, $this->metadataFactory, $this->validatorFactory, $this->messageInterpolator); + + foreach ($groupChain->getGroups() as $group) + { + $closure($walker, $group); + } + + foreach ($groupChain->getGroupSequences() as $sequence) + { + $violationCount = count($walker->getViolations()); + + foreach ($sequence as $group) + { + $closure($walker, $group); + + if (count($walker->getViolations()) > $violationCount) + { + break; + } + } + } + + return $walker->getViolations(); + } + + protected function buildSimpleGroupChain($groups) + { + if (is_null($groups)) + { + $groups = array(Constraint::DEFAULT_GROUP); + } + else + { + $groups = (array)$groups; + } + + $chain = new GroupChain(); + + foreach ($groups as $group) + { + $chain->addGroup($group); + } + + return $chain; + } + + protected function buildGroupChain(ClassMetadata $metadata, $groups) + { + if (is_null($groups)) + { + $groups = array(Constraint::DEFAULT_GROUP); + } + else + { + $groups = (array)$groups; + } + + $chain = new GroupChain(); + + foreach ($groups as $group) + { + if ($group == Constraint::DEFAULT_GROUP && $metadata->hasGroupSequence()) + { + $chain->addGroupSequence($metadata->getGroupSequence()); + } + else + { + $chain->addGroup($group); + } + } + + return $chain; + } +} \ No newline at end of file diff --git a/src/Symfony/Components/Validator/ValidatorInterface.php b/src/Symfony/Components/Validator/ValidatorInterface.php new file mode 100644 index 000000000000..ef256274e877 --- /dev/null +++ b/src/Symfony/Components/Validator/ValidatorInterface.php @@ -0,0 +1,22 @@ + + * @version SVN: $Id: ValidatorInterface.php 138 2010-01-18 22:05:14Z flo $ + */ +interface ValidatorInterface +{ + public function validate($object, $groups = null); + + public function validateProperty($object, $property, $groups = null); + + public function validatePropertyValue($class, $property, $value, $groups = null); + + public function validateValue($value, Constraint $constraint, $groups = null); +} \ No newline at end of file diff --git a/src/Symfony/Framework/FoundationBundle/DependencyInjection/WebExtension.php b/src/Symfony/Framework/FoundationBundle/DependencyInjection/WebExtension.php index 48f79911852d..05d133d26105 100644 --- a/src/Symfony/Framework/FoundationBundle/DependencyInjection/WebExtension.php +++ b/src/Symfony/Framework/FoundationBundle/DependencyInjection/WebExtension.php @@ -6,6 +6,8 @@ use Symfony\Components\DependencyInjection\Loader\XmlFileLoader; use Symfony\Components\DependencyInjection\BuilderConfiguration; use Symfony\Components\DependencyInjection\Reference; +use Symfony\Components\DependencyInjection\Definition; +use Symfony\Components\DependencyInjection\FileResource; /* * This file is part of the Symfony framework. @@ -29,8 +31,20 @@ class WebExtension extends LoaderExtension 'templating' => 'templating.xml', 'web' => 'web.xml', 'user' => 'user.xml', + // validation.xml conflicts with the naming convention for XML + // validation mapping files, so call it validator.xml + 'validation' => 'validator.xml', ); + protected $bundleDirs = array(); + protected $bundles = array(); + + public function __construct(array $bundleDirs, array $bundles) + { + $this->bundleDirs = $bundleDirs; + $this->bundles = $bundles; + } + /** * Loads the web configuration. * @@ -77,6 +91,82 @@ public function configLoad($config, BuilderConfiguration $configuration) } } + if (isset($config['validation'])) { + if ($config['validation']) { + if (!$configuration->hasDefinition('validator')) { + $loader = new XmlFileLoader(__DIR__.'/../Resources/config'); + $configuration->merge($loader->load($this->resources['validation'])); + } + + $xmlMappingFiles = array(); + $yamlMappingFiles = array(); + $messageFiles = array(); + + // default entries by the framework + $xmlMappingFiles[] = __DIR__.'/../../../Components/Form/Resources/config/validation.xml'; + $messageFiles[] = __DIR__ . '/../../../Components/Validator/Resources/i18n/messages.en.xml'; + $messageFiles[] = __DIR__ . '/../../../Components/Form/Resources/i18n/messages.en.xml'; + + foreach ($this->bundles as $className) { + $tmp = dirname(str_replace('\\', '/', $className)); + $namespace = str_replace('/', '\\', dirname($tmp)); + $bundle = basename($tmp); + + foreach ($this->bundleDirs as $dir) { + if (file_exists($file = $dir.'/'.$bundle.'/Resources/config/validation.xml')) { + $xmlMappingFiles[] = realpath($file); + } + if (file_exists($file = $dir.'/'.$bundle.'/Resources/config/validation.yml')) { + $yamlMappingFiles[] = realpath($file); + } + + // TODO do we really want the message files of all cultures? + foreach (glob($dir.'/'.$bundle.'/Resources/i18n/messages.*.xml') as $file) { + $messageFiles[] = realpath($file); + } + } + } + + $xmlFilesLoader = new Definition( + $configuration->getParameter('validator.mapping.loader.xml_files_loader.class'), + array($xmlMappingFiles) + ); + + $yamlFilesLoader = new Definition( + $configuration->getParameter('validator.mapping.loader.yaml_files_loader.class'), + array($yamlMappingFiles) + ); + + $configuration->setDefinition('validator.mapping.loader.xml_files_loader', $xmlFilesLoader); + $configuration->setDefinition('validator.mapping.loader.yaml_files_loader', $yamlFilesLoader); + $configuration->setParameter('validator.message_interpolator.files', $messageFiles); + + foreach ($xmlMappingFiles as $file) { + $configuration->addResource(new FileResource($file)); + } + + foreach ($yamlMappingFiles as $file) { + $configuration->addResource(new FileResource($file)); + } + + foreach ($messageFiles as $file) { + $configuration->addResource(new FileResource($file)); + } + + if (isset($config['validation']['annotations']) && $config['validation']['annotations'] === true) { + $annotationLoader = new Definition($configuration->getParameter('validator.mapping.loader.annotation_loader.class')); + $configuration->setDefinition('validator.mapping.loader.annotation_loader', $annotationLoader); + + $loader = $configuration->getDefinition('validator.mapping.loader.loader_chain'); + $arguments = $loader->getArguments(); + array_unshift($arguments, new Reference('validator.mapping.loader.annotation_loader')); + $loader->setArguments($arguments); + } + } elseif ($configuration->hasDefinition('validator')) { + $configuration->getDefinition('validator')->clearAnnotations(); + } + } + return $configuration; } diff --git a/src/Symfony/Framework/FoundationBundle/FoundationBundle.php b/src/Symfony/Framework/FoundationBundle/FoundationBundle.php index fc7c60054374..c6ddcfd073ae 100644 --- a/src/Symfony/Framework/FoundationBundle/FoundationBundle.php +++ b/src/Symfony/Framework/FoundationBundle/FoundationBundle.php @@ -36,7 +36,7 @@ class FoundationBundle extends Bundle */ public function buildContainer(ContainerInterface $container) { - Loader::registerExtension(new WebExtension()); + Loader::registerExtension(new WebExtension($container->getParameter('kernel.bundle_dirs'), $container->getParameter('kernel.bundles'))); $dirs = array('%kernel.root_dir%/views/%%bundle%%/%%controller%%/%%name%%%%format%%.%%renderer%%'); foreach ($container->getParameter('kernel.bundle_dirs') as $dir) { diff --git a/src/Symfony/Framework/FoundationBundle/Resources/config/validator.xml b/src/Symfony/Framework/FoundationBundle/Resources/config/validator.xml new file mode 100644 index 000000000000..6e86975764a4 --- /dev/null +++ b/src/Symfony/Framework/FoundationBundle/Resources/config/validator.xml @@ -0,0 +1,54 @@ + + + + + + Symfony\Components\Validator\Validator + Symfony\Components\Validator\Extension\DependencyInjectionValidatorFactory + Symfony\Components\Validator\MessageInterpolator\XliffMessageInterpolator + Symfony\Components\Validator\Mapping\ClassMetadataFactory + Symfony\Components\Validator\Mapping\Loader\LoaderChain + Symfony\Components\Validator\Mapping\Loader\StaticMethodLoader + Symfony\Components\Validator\Mapping\Loader\AnnotationLoader + Symfony\Components\Validator\Mapping\Loader\XmlFileLoader + Symfony\Components\Validator\Mapping\Loader\YamlFileLoader + Symfony\Components\Validator\Mapping\Loader\XmlFilesLoader + Symfony\Components\Validator\Mapping\Loader\YamlFilesLoader + loadValidatorMetadata + + + + + + + + + + + + + + + + + %validator.message_interpolator.files% + + + + + + + + + + + + %validator.mapping.loader.static_method_loader.method_name% + + + + diff --git a/src/Symfony/Framework/FoundationBundle/Tests/DependencyInjection/WebExtensionTest.php b/src/Symfony/Framework/FoundationBundle/Tests/DependencyInjection/WebExtensionTest.php index b144e7be80b2..b3dbd0ce3b3b 100644 --- a/src/Symfony/Framework/FoundationBundle/Tests/DependencyInjection/WebExtensionTest.php +++ b/src/Symfony/Framework/FoundationBundle/Tests/DependencyInjection/WebExtensionTest.php @@ -20,13 +20,13 @@ class WebExtensionTest extends TestCase public function testConfigLoad() { $configuration = new BuilderConfiguration(); - $loader = new WebExtension(); + $loader = $this->getWebExtension(); $configuration = $loader->configLoad(array(), $configuration); $this->assertEquals('Symfony\\Framework\\FoundationBundle\\Listener\\RequestParser', $configuration->getParameter('request_parser.class'), '->webLoad() loads the web.xml file if not already loaded'); $configuration = new BuilderConfiguration(); - $loader = new WebExtension(); + $loader = $this->getWebExtension(); $configuration = $loader->configLoad(array('profiler' => true), $configuration); $this->assertEquals('Symfony\\Framework\\FoundationBundle\\Profiler', $configuration->getParameter('profiler.class'), '->configLoad() loads the collectors.xml file if not already loaded'); @@ -39,7 +39,7 @@ public function testConfigLoad() public function testUserLoad() { $configuration = new BuilderConfiguration(); - $loader = new WebExtension(); + $loader = $this->getWebExtension(); $configuration = $loader->userLoad(array(), $configuration); $this->assertEquals('Symfony\\Framework\\FoundationBundle\\User', $configuration->getParameter('user.class'), '->userLoad() loads the user.xml file if not already loaded'); @@ -48,9 +48,30 @@ public function testUserLoad() public function testTemplatingLoad() { $configuration = new BuilderConfiguration(); - $loader = new WebExtension(); + $loader = $this->getWebExtension(); $configuration = $loader->templatingLoad(array(), $configuration); $this->assertEquals('Symfony\\Framework\\FoundationBundle\\Templating\\Engine', $configuration->getParameter('templating.engine.class'), '->templatingLoad() loads the templating.xml file if not already loaded'); } + + public function testValidationLoad() + { + $configuration = new BuilderConfiguration(); + $loader = $this->getWebExtension(); + + $configuration = $loader->configLoad(array('validation' => true), $configuration); + $this->assertEquals('Symfony\Components\Validator\Validator', $configuration->getParameter('validator.class'), '->validationLoad() loads the validation.xml file if not already loaded'); + $this->assertFalse($configuration->hasDefinition('validator.mapping.loader.annotation_loader'), '->validationLoad() doesn\'t load the annotations service unless its needed'); + + $configuration = $loader->configLoad(array('validation' => array('annotations' => true)), $configuration); + $this->assertTrue($configuration->hasDefinition('validator.mapping.loader.annotation_loader'), '->validationLoad() loads the annotations service'); + } + + public function getWebExtension() { + return new WebExtension(array( + 'Symfony\\Framework' => __DIR__ . '/../../../Framework', + ), array( + 'FoundationBundle', + )); + } } diff --git a/tests/Symfony/Tests/Components/File/FileTest.php b/tests/Symfony/Tests/Components/File/FileTest.php new file mode 100644 index 000000000000..6e1d9893494b --- /dev/null +++ b/tests/Symfony/Tests/Components/File/FileTest.php @@ -0,0 +1,74 @@ +file = new File(__DIR__.'/Fixtures/test.gif'); + } + + public function testGetPathReturnsAbsolutePath() + { + $this->assertEquals(__DIR__.'/Fixtures/test.gif', $this->file->getPath()); + } + + public function testGetNameReturnsNameWithExtension() + { + $this->assertEquals('test.gif', $this->file->getName()); + } + + public function testGetExtensionReturnsExtensionWithDot() + { + $this->assertEquals('.gif', $this->file->getExtension()); + } + + public function testGetDirectoryReturnsDirectoryName() + { + $this->assertEquals(__DIR__.'/Fixtures', $this->file->getDirectory()); + } + + public function testGetMimeTypeUsesMimeTypeGuessers() + { + $guesser = $this->createMockGuesser($this->file->getPath(), 'image/gif'); + + MimeTypeGuesser::getInstance()->register($guesser); + + $this->assertEquals('image/gif', $this->file->getMimeType()); + } + + public function testGetDefaultExtensionIsBasedOnMimeType() + { + $file = new File(__DIR__.'/Fixtures/test'); + $guesser = $this->createMockGuesser($file->getPath(), 'image/gif'); + + MimeTypeGuesser::getInstance()->register($guesser); + + $this->assertEquals('.gif', $file->getDefaultExtension()); + } + + public function testSizeReturnsFileSize() + { + $this->assertEquals(filesize($this->file->getPath()), $this->file->size()); + } + + protected function createMockGuesser($path, $mimeType) + { + $guesser = $this->getMock('Symfony\Components\File\MimeType\MimeTypeGuesserInterface'); + $guesser->expects($this->once()) + ->method('guess') + ->with($this->equalTo($path)) + ->will($this->returnValue($mimeType)); + + return $guesser; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/File/UploadedFileTest.php b/tests/Symfony/Tests/Components/File/UploadedFileTest.php new file mode 100644 index 000000000000..f94eb465de32 --- /dev/null +++ b/tests/Symfony/Tests/Components/File/UploadedFileTest.php @@ -0,0 +1,45 @@ +setExpectedException('Symfony\Components\File\Exception\FileException'); + + new UploadedFile( + __DIR__.'/Fixtures/test.gif', + 'original.gif', + 'image/gif', + filesize(__DIR__.'/Fixtures/test.gif'), + UPLOAD_ERR_OK + ); + } + } + + public function testErrorIsOkByDefault() + { + // we can't change this setting without modifying php.ini :( + if (ini_get('file_uploads')) + { + $file = new UploadedFile( + __DIR__.'/Fixtures/test.gif', + 'original.gif', + 'image/gif', + filesize(__DIR__.'/Fixtures/test.gif'), + null + ); + + $this->assertEquals(UPLOAD_ERR_OK, $file->getError()); + } + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/CheckboxFieldTest.php b/tests/Symfony/Tests/Components/Form/CheckboxFieldTest.php new file mode 100644 index 000000000000..4245c1220fe5 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/CheckboxFieldTest.php @@ -0,0 +1,22 @@ +setData(true); + + $html = ''; + + $this->assertEquals($html, $field->render(array( + 'class' => 'foobar', + ))); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/ChoiceFieldTest.php b/tests/Symfony/Tests/Components/Form/ChoiceFieldTest.php new file mode 100644 index 000000000000..064bc193d867 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/ChoiceFieldTest.php @@ -0,0 +1,376 @@ + 'Bernhard', + 'b' => 'Fabien', + 'c' => 'Kris', + 'd' => 'Jon', + 'e' => 'Roman', + ); + + protected $preferredChoices = array('d', 'e'); + + protected $groupedChoices = array( + 'Symfony' => array( + 'a' => 'Bernhard', + 'b' => 'Fabien', + 'c' => 'Kris', + ), + 'Doctrine' => array( + 'd' => 'Jon', + 'e' => 'Roman', + ) + ); + + public function testBindSingleNonExpanded() + { + $field = new ChoiceField('name', array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $field->bind('b'); + + $this->assertEquals('b', $field->getData()); + $this->assertEquals('b', $field->getDisplayedData()); + } + + public function testRenderSingleNonExpanded() + { + $field = new ChoiceField('name', array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $field->setData('b'); + + $html = << + + + + + + +EOF; + + $this->assertEquals($html, $field->render(array( + 'class' => 'foobar', + ))); + } + + public function testRenderSingleNonExpanded_translateChoices() + { + $translator = $this->getMock('Symfony\Components\I18N\TranslatorInterface'); + $translator->expects($this->any()) + ->method('translate') + ->will($this->returnCallback(function($text) { + return 'translated['.$text.']'; + })); + + $field = new ChoiceField('name', array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + 'translate_choices' => true, + )); + + $field->setTranslator($translator); + $field->setData('b'); + + $html = << + + + + + + +EOF; + + $this->assertEquals($html, $field->render(array( + 'class' => 'foobar', + ))); + } + + public function testRenderSingleNonExpanded_disabled() + { + $field = new ChoiceField('name', array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + 'disabled' => true, + )); + + + $html = << + + + + + + +EOF; + + $this->assertEquals($html, $field->render()); + } + + public function testRenderSingleNonExpandedWithPreferred() + { + $field = new ChoiceField('name', array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + 'preferred_choices' => $this->preferredChoices, + 'separator' => '---', + )); + + $field->setData('d'); + + $html = << + + + + + + + +EOF; + + $this->assertEquals($html, $field->render()); + } + + public function testRenderSingleNonExpandedWithGroups() + { + $field = new ChoiceField('name', array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->groupedChoices, + )); + + $html = << + + + + + + + + + + +EOF; + + $this->assertEquals($html, $field->render()); + } + + public function testRenderSingleNonExpandedNonRequired() + { + $field = new ChoiceField('name', array( + 'multiple' => false, + 'expanded' => false, + 'choices' => $this->choices, + 'empty_value' => 'empty', + )); + + $field->setData(null); + $field->setRequired(false); + + $html = << + + + + + + + +EOF; + + $this->assertEquals($html, $field->render()); + } + + public function testBindMultipleNonExpanded() + { + $field = new ChoiceField('name', array( + 'multiple' => true, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $field->bind(array('a', 'b')); + + $this->assertEquals(array('a', 'b'), $field->getData()); + $this->assertEquals(array('a', 'b'), $field->getDisplayedData()); + } + + public function testRenderMultipleNonExpanded() + { + $field = new ChoiceField('name', array( + 'multiple' => true, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $field->setData(array('a', 'b')); + + $html = << + + + + + + +EOF; + + $this->assertEquals($html, $field->render()); + } + + public function testBindSingleExpanded() + { + $field = new ChoiceField('name', array( + 'multiple' => true, + 'expanded' => false, + 'choices' => $this->choices, + )); + + $field->bind('b'); + + $this->assertEquals('b', $field->getData()); + $this->assertEquals('b', $field->getDisplayedData()); + } + + public function testRenderSingleExpanded() + { + $field = new ChoiceField('name', array( + 'multiple' => false, + 'expanded' => true, + 'choices' => $this->choices, + )); + + $field->setData('b'); + + $html = << + + + + + +EOF; + + $this->assertEquals($html, $field->render()); + } + + public function testRenderSingleExpanded_translateChoices() + { + $translator = $this->getMock('Symfony\Components\I18N\TranslatorInterface'); + $translator->expects($this->any()) + ->method('translate') + ->will($this->returnCallback(function($text) { + return 'translated['.$text.']'; + })); + + $field = new ChoiceField('name', array( + 'multiple' => false, + 'expanded' => true, + 'choices' => $this->choices, + 'translate_choices' => true, + )); + + $field->setTranslator($translator); + $field->setData('b'); + + $html = << + + + + + +EOF; + + $this->assertEquals($html, $field->render()); + } + + public function testRenderSingleExpandedWithPreferred() + { + $field = new ChoiceField('name', array( + 'multiple' => false, + 'expanded' => true, + 'choices' => $this->choices, + 'preferred_choices' => $this->preferredChoices, + )); + + $html = << + + + + + +EOF; + + $this->assertEquals($html, $field->render()); + } + + public function testBindMultipleExpanded() + { + $field = new ChoiceField('name', array( + 'multiple' => true, + 'expanded' => true, + 'choices' => $this->choices, + )); + + $field->bind(array('a' => 'a', 'b' => 'b')); + + $this->assertSame(array('a', 'b'), $field->getData()); + $this->assertSame(true, $field['a']->getData()); + $this->assertSame(true, $field['b']->getData()); + $this->assertSame(null, $field['c']->getData()); + $this->assertSame(null, $field['d']->getData()); + $this->assertSame(null, $field['e']->getData()); + $this->assertSame('1', $field['a']->getDisplayedData()); + $this->assertSame('1', $field['b']->getDisplayedData()); + $this->assertSame('', $field['c']->getDisplayedData()); + $this->assertSame('', $field['d']->getDisplayedData()); + $this->assertSame('', $field['e']->getDisplayedData()); + $this->assertSame(array('a' => '1', 'b' => '1', 'c' => '', 'd' => '', 'e' => ''), $field->getDisplayedData()); + } + + public function testRenderMultipleExpanded() + { + $field = new ChoiceField('name', array( + 'multiple' => true, + 'expanded' => true, + 'choices' => $this->choices, + )); + + $field->setData(array('a', 'b')); + + $html = << + + + + + +EOF; + + $this->assertEquals($html, $field->render()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/CollectionFieldTest.php b/tests/Symfony/Tests/Components/Form/CollectionFieldTest.php new file mode 100644 index 000000000000..48e06b61833f --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/CollectionFieldTest.php @@ -0,0 +1,100 @@ +assertEquals(0, count($field)); + } + + public function testSetDataAdjustsSize() + { + $field = new CollectionField(new TestField('emails')); + $field->setData(array('foo@foo.com', 'foo@bar.com')); + + $this->assertTrue($field[0] instanceof TestField); + $this->assertTrue($field[1] instanceof TestField); + $this->assertEquals(2, count($field)); + $this->assertEquals('foo@foo.com', $field[0]->getData()); + $this->assertEquals('foo@bar.com', $field[1]->getData()); + } + + public function testThrowsExceptionIfObjectIsNotTraversable() + { + $field = new CollectionField(new TestField('emails')); + $this->setExpectedException('Symfony\Components\Form\Exception\UnexpectedTypeException'); + $field->setData(new \stdClass()); + } + + public function testModifiableCollectionsContainExtraField() + { + $field = new CollectionField(new TestField('emails'), array( + 'modifiable' => true, + )); + $field->setData(array('foo@bar.com')); + + $this->assertTrue($field['0'] instanceof TestField); + $this->assertTrue($field['$$key$$'] instanceof TestField); + $this->assertEquals(2, count($field)); + } + + public function testNotResizedIfBoundWithMissingData() + { + $field = new CollectionField(new TestField('emails')); + $field->setData(array('foo@foo.com', 'bar@bar.com')); + $field->bind(array('foo@bar.com')); + + $this->assertTrue($field->has('0')); + $this->assertTrue($field->has('1')); + $this->assertEquals('foo@bar.com', $field[0]->getData()); + $this->assertEquals(null, $field[1]->getData()); + } + + public function testResizedIfBoundWithMissingDataAndModifiable() + { + $field = new CollectionField(new TestField('emails'), array( + 'modifiable' => true, + )); + $field->setData(array('foo@foo.com', 'bar@bar.com')); + $field->bind(array('foo@bar.com')); + + $this->assertTrue($field->has('0')); + $this->assertFalse($field->has('1')); + $this->assertEquals('foo@bar.com', $field[0]->getData()); + } + + public function testNotResizedIfBoundWithExtraData() + { + $field = new CollectionField(new TestField('emails')); + $field->setData(array('foo@bar.com')); + $field->bind(array('foo@foo.com', 'bar@bar.com')); + + $this->assertTrue($field->has('0')); + $this->assertFalse($field->has('1')); + $this->assertEquals('foo@foo.com', $field[0]->getData()); + } + + public function testResizedIfBoundWithExtraDataAndModifiable() + { + $field = new CollectionField(new TestField('emails'), array( + 'modifiable' => true, + )); + $field->setData(array('foo@bar.com')); + $field->bind(array('foo@foo.com', 'bar@bar.com')); + + $this->assertTrue($field->has('0')); + $this->assertTrue($field->has('1')); + $this->assertEquals('foo@foo.com', $field[0]->getData()); + $this->assertEquals('bar@bar.com', $field[1]->getData()); + } +} diff --git a/tests/Symfony/Tests/Components/Form/DateFieldTest.php b/tests/Symfony/Tests/Components/Form/DateFieldTest.php new file mode 100644 index 000000000000..1300c4d12ed3 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/DateFieldTest.php @@ -0,0 +1,216 @@ + 'input', 'type' => DateField::DATETIME)); + + $field->setLocale('de_AT'); + $field->bind('2.6.2010'); + + $this->assertDateTimeEquals(new \DateTime('2010-06-02 UTC'), $field->getData()); + $this->assertEquals('02.06.2010', $field->getDisplayedData()); + } + + public function testBind_fromInput_string() + { + $field = new DateField('name', array('widget' => 'input', 'type' => DateField::STRING)); + + $field->setLocale('de_AT'); + $field->bind('2.6.2010'); + + $this->assertEquals('2010-06-02', $field->getData()); + $this->assertEquals('02.06.2010', $field->getDisplayedData()); + } + + public function testBind_fromInput_timestamp() + { + $field = new DateField('name', array('widget' => 'input', 'type' => DateField::TIMESTAMP)); + + $field->setLocale('de_AT'); + $field->bind('2.6.2010'); + + $dateTime = new \DateTime('2010-06-02 UTC'); + + $this->assertEquals($dateTime->format('U'), $field->getData()); + $this->assertEquals('02.06.2010', $field->getDisplayedData()); + } + + public function testBind_fromInput_raw() + { + $field = new DateField('name', array( + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + 'widget' => 'input', + 'type' => DateField::RAW, + )); + + $field->setLocale('de_AT'); + $field->bind('2.6.2010'); + + $output = array( + 'day' => '2', + 'month' => '6', + 'year' => '2010', + ); + + $this->assertEquals($output, $field->getData()); + $this->assertEquals('02.06.2010', $field->getDisplayedData()); + } + + public function testBind_fromChoice() + { + $field = new DateField('name', array('widget' => DateField::CHOICE)); + + $input = array( + 'day' => '2', + 'month' => '6', + 'year' => '2010', + ); + + $field->setLocale('de_AT'); + $field->bind($input); + + $dateTime = new \DateTime('2010-06-02 UTC'); + + $this->assertDateTimeEquals($dateTime, $field->getData()); + $this->assertEquals($input, $field->getDisplayedData()); + } + + public function testSetData_differentTimezones() + { + $field = new DateField('name', array( + 'data_timezone' => 'America/New_York', + 'user_timezone' => 'Pacific/Tahiti', + // don't do this test with DateTime, because it leads to wrong results! + 'type' => DateField::STRING, + 'widget' => 'input', + )); + + $field->setLocale('de_AT'); + $field->setData('2010-06-02'); + + $this->assertEquals('01.06.2010', $field->getDisplayedData()); + } + + public function testRenderAsInput() + { + $field = new DateField('name', array('widget' => 'input')); + + $field->setLocale('de_AT'); + $field->setData(new \DateTime('2010-06-02 UTC')); + + $html = ''; + + $this->assertEquals($html, $field->render(array( + 'class' => 'foobar', + ))); + } + + public function testRenderAsInputWithFormat() + { + $field = new DateField('name', array('widget' => 'input', 'format' => 'short')); + + $field->setLocale('de_AT'); + $field->setData(new \DateTime('2010-06-02 UTC')); + + $html = ''; + + $this->assertEquals($html, $field->render()); + } + + public function testRenderAsChoice() + { + $field = new DateField('name', array( + 'years' => array(2010, 2011), + 'months' => array(6, 7), + 'days' => array(1, 2), + 'widget' => DateField::CHOICE, + )); + + $field->setLocale('de_AT'); + $field->setData(new \DateTime('2010-06-02 UTC')); + + $html = << + + +.. +EOF; + + $this->assertEquals($html, $field->render(array( + 'class' => 'foobar', + ))); + } + + public function testRenderAsChoiceNonRequired() + { + $field = new DateField('name', array( + 'years' => array(2010, 2011), + 'months' => array(6, 7), + 'days' => array(1, 2), + 'widget' => DateField::CHOICE, + )); + + $field->setLocale('de_AT'); + $field->setRequired(false); + + $html = << + + + +.. +EOF; + + $this->assertEquals($html, $field->render()); + } + + public function testRenderAsChoiceWithPattern() + { + $field = new DateField('name', array( + 'years' => array(2010, 2011), + 'months' => array(6, 7), + 'days' => array(1, 2), + 'widget' => DateField::CHOICE, + 'pattern' => '%day%---%month%---%year%', + )); + + $html = << + + +------ +EOF; + + $this->assertEquals($html, $field->render()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/DateTimeFieldTest.php b/tests/Symfony/Tests/Components/Form/DateTimeFieldTest.php new file mode 100644 index 000000000000..5131b3cb2496 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/DateTimeFieldTest.php @@ -0,0 +1,200 @@ + 'UTC', + 'user_timezone' => 'UTC', + 'date_widget' => DateField::CHOICE, + 'time_widget' => TimeField::CHOICE, + 'type' => DateTimeField::DATETIME, + )); + + $field->bind(array( + 'date' => array( + 'day' => '2', + 'month' => '6', + 'year' => '2010', + ), + 'time' => array( + 'hour' => '3', + 'minute' => '4', + ), + )); + + $dateTime = new \DateTime('2010-06-02 03:04:00 UTC'); + + $this->assertDateTimeEquals($dateTime, $field->getData()); + } + + public function testBind_string() + { + $field = new DateTimeField('name', array( + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + 'type' => DateTimeField::STRING, + 'date_widget' => DateField::CHOICE, + 'time_widget' => TimeField::CHOICE, + )); + + $field->bind(array( + 'date' => array( + 'day' => '2', + 'month' => '6', + 'year' => '2010', + ), + 'time' => array( + 'hour' => '3', + 'minute' => '4', + ), + )); + + $this->assertEquals('2010-06-02 03:04:00', $field->getData()); + } + + public function testBind_timestamp() + { + $field = new DateTimeField('name', array( + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + 'type' => DateTimeField::TIMESTAMP, + 'date_widget' => DateField::CHOICE, + 'time_widget' => TimeField::CHOICE, + )); + + $field->bind(array( + 'date' => array( + 'day' => '2', + 'month' => '6', + 'year' => '2010', + ), + 'time' => array( + 'hour' => '3', + 'minute' => '4', + ), + )); + + $dateTime = new \DateTime('2010-06-02 03:04:00 UTC'); + + $this->assertEquals($dateTime->format('U'), $field->getData()); + } + + public function testBind_withSeconds() + { + $field = new DateTimeField('name', array( + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + 'date_widget' => DateField::CHOICE, + 'time_widget' => TimeField::CHOICE, + 'type' => DateTimeField::DATETIME, + 'with_seconds' => true, + )); + + $field->setData(new \DateTime('2010-06-02 03:04:05 UTC')); + + $input = array( + 'date' => array( + 'day' => '2', + 'month' => '6', + 'year' => '2010', + ), + 'time' => array( + 'hour' => '3', + 'minute' => '4', + 'second' => '5', + ), + ); + + $field->bind($input); + + $this->assertDateTimeEquals(new \DateTime('2010-06-02 03:04:05 UTC'), $field->getData()); + } + + public function testBind_differentTimezones() + { + $field = new DateTimeField('name', array( + 'data_timezone' => 'America/New_York', + 'user_timezone' => 'Pacific/Tahiti', + 'date_widget' => DateField::CHOICE, + 'time_widget' => TimeField::CHOICE, + // don't do this test with DateTime, because it leads to wrong results! + 'type' => DateTimeField::STRING, + 'with_seconds' => true, + )); + + $dateTime = new \DateTime('2010-06-02 03:04:05 Pacific/Tahiti'); + + $field->bind(array( + 'date' => array( + 'day' => (int)$dateTime->format('d'), + 'month' => (int)$dateTime->format('m'), + 'year' => (int)$dateTime->format('Y'), + ), + 'time' => array( + 'hour' => (int)$dateTime->format('H'), + 'minute' => (int)$dateTime->format('i'), + 'second' => (int)$dateTime->format('s'), + ), + )); + + $dateTime->setTimezone(new \DateTimeZone('America/New_York')); + + $this->assertEquals($dateTime->format('Y-m-d H:i:s'), $field->getData()); + } + + public function testRender() + { + $field = new DateTimeField('name', array( + 'years' => array(2010, 2011), + 'months' => array(6, 7), + 'days' => array(1, 2), + 'hours' => array(3, 4), + 'minutes' => array(5, 6), + 'seconds' => array(7, 8), + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + 'date_widget' => DateField::CHOICE, + 'time_widget' => TimeField::CHOICE, + 'type' => DateTimeField::DATETIME, + 'with_seconds' => true, + )); + + $field->setData(new \DateTime('2010-06-02 03:04:05 UTC')); + + $html = << + + +.. +:: +EOF; + + $this->assertEquals($html, $field->render(array('class' => 'foobar'))); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/DateTimeTestCase.php b/tests/Symfony/Tests/Components/Form/DateTimeTestCase.php new file mode 100644 index 000000000000..f53932aa27fe --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/DateTimeTestCase.php @@ -0,0 +1,11 @@ +format('c'), $actual->format('c')); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/FieldGroupTest.php b/tests/Symfony/Tests/Components/Form/FieldGroupTest.php new file mode 100644 index 000000000000..a7633a78b9d4 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/FieldGroupTest.php @@ -0,0 +1,805 @@ +locales[] = $locale; + } +} + + +class FieldGroupTest extends \PHPUnit_Framework_TestCase +{ + public function testSupportsArrayAccess() + { + $group = new FieldGroup('author'); + $group->add($this->createMockField('firstName')); + $this->assertEquals($group->get('firstName'), $group['firstName']); + $this->assertTrue(isset($group['firstName'])); + } + + public function testSupportsUnset() + { + $group = new FieldGroup('author'); + $group->add($this->createMockField('firstName')); + unset($group['firstName']); + $this->assertFalse(isset($group['firstName'])); + } + + public function testDoesNotSupportAddingFields() + { + $group = new FieldGroup('author'); + $this->setExpectedException('LogicException'); + $group[] = $this->createMockField('lastName'); + } + + public function testSupportsCountable() + { + $group = new FieldGroup('group'); + $group->add($this->createMockField('firstName')); + $group->add($this->createMockField('lastName')); + $this->assertEquals(2, count($group)); + + $group->add($this->createMockField('australian')); + $this->assertEquals(3, count($group)); + } + + public function testSupportsIterable() + { + $group = new FieldGroup('group'); + $group->add($field1 = $this->createMockField('field1')); + $group->add($field2 = $this->createMockField('field2')); + $group->add($field3 = $this->createMockField('field3')); + + $expected = array( + 'field1' => $field1, + 'field2' => $field2, + 'field3' => $field3, + ); + + $this->assertEquals($expected, iterator_to_array($group)); + } + + public function testIsBound() + { + $group = new FieldGroup('author'); + $this->assertFalse($group->isBound()); + $group->bind(array('firstName' => 'Bernhard')); + $this->assertTrue($group->isBound()); + } + + public function testValidIfAllFieldsAreValid() + { + $group = new FieldGroup('author'); + $group->add($this->createValidMockField('firstName')); + $group->add($this->createValidMockField('lastName')); + + $group->bind(array('firstName' => 'Bernhard', 'lastName' => 'Potencier')); + + $this->assertTrue($group->isValid()); + } + + public function testInvalidIfFieldIsInvalid() + { + $group = new FieldGroup('author'); + $group->add($this->createInvalidMockField('firstName')); + $group->add($this->createValidMockField('lastName')); + + $group->bind(array('firstName' => 'Bernhard', 'lastName' => 'Potencier')); + + $this->assertFalse($group->isValid()); + } + + public function testInvalidIfBoundWithExtraFields() + { + $group = new FieldGroup('author'); + $group->add($this->createValidMockField('firstName')); + $group->add($this->createValidMockField('lastName')); + + $group->bind(array('foo' => 'bar', 'firstName' => 'Bernhard', 'lastName' => 'Potencier')); + + $this->assertTrue($group->isBoundWithExtraFields()); + } + + public function testBindForwardsBoundValues() + { + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('bind') + ->with($this->equalTo('Bernhard')); + + $group = new FieldGroup('author'); + $group->add($field); + + $group->bind(array('firstName' => 'Bernhard')); + } + + public function testBindForwardsNullIfValueIsMissing() + { + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('bind') + ->with($this->equalTo(null)); + + $group = new FieldGroup('author'); + $group->add($field); + + $group->bind(array()); + } + + public function testAddErrorMapsFieldValidationErrorsOntoFields() + { + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('addError') + ->with($this->equalTo('Message')); + + $group = new FieldGroup('author'); + $group->add($field); + + $group->addError('Message', new PropertyPath('fields[firstName].data'), FieldGroup::FIELD_ERROR); + } + + public function testAddErrorKeepsFieldValidationErrorsIfFieldNotFound() + { + $field = $this->createMockField('foo'); + $field->expects($this->never()) + ->method('addError'); + + $group = new FieldGroup('author'); + $group->add($field); + + $group->addError('Message', new PropertyPath('fields[bar].data'), FieldGroup::FIELD_ERROR); + + $this->assertEquals(array('Message'), $group->getErrors()); + } + + public function testAddErrorKeepsFieldValidationErrorsIfFieldIsHidden() + { + $field = $this->createMockField('firstName'); + $field->expects($this->any()) + ->method('isHidden') + ->will($this->returnValue(true)); + $field->expects($this->never()) + ->method('addError'); + + $group = new FieldGroup('author'); + $group->add($field); + + $group->addError('Message', new PropertyPath('fields[firstName].data'), FieldGroup::FIELD_ERROR); + + $this->assertEquals(array('Message'), $group->getErrors()); + } + + public function testAddErrorMapsDataValidationErrorsOntoFields() + { + // path is expected to point at "firstName" + $expectedPath = new PropertyPath('firstName'); + + $field = $this->createMockField('firstName'); + $field->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue(new PropertyPath('firstName'))); + $field->expects($this->once()) + ->method('addError') + ->with($this->equalTo('Message'), $this->equalTo($expectedPath), $this->equalTo(FieldGroup::DATA_ERROR)); + + $group = new FieldGroup('author'); + $group->add($field); + + $group->addError('Message', new PropertyPath('firstName'), FieldGroup::DATA_ERROR); + } + + public function testAddErrorKeepsDataValidationErrorsIfFieldNotFound() + { + $field = $this->createMockField('foo'); + $field->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue(new PropertyPath('foo'))); + $field->expects($this->never()) + ->method('addError'); + + $group = new FieldGroup('author'); + $group->add($field); + + $group->addError('Message', new PropertyPath('bar'), FieldGroup::DATA_ERROR); + } + + public function testAddErrorKeepsDataValidationErrorsIfFieldIsHidden() + { + $field = $this->createMockField('firstName'); + $field->expects($this->any()) + ->method('isHidden') + ->will($this->returnValue(true)); + $field->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue(new PropertyPath('firstName'))); + $field->expects($this->never()) + ->method('addError'); + + $group = new FieldGroup('author'); + $group->add($field); + + $group->addError('Message', new PropertyPath('firstName'), FieldGroup::DATA_ERROR); + } + + public function testAddErrorMapsDataValidationErrorsOntoNestedFields() + { + // path is expected to point at "street" + $expectedPath = new PropertyPath('address.street'); + $expectedPath->next(); + + $field = $this->createMockField('address'); + $field->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue(new PropertyPath('address'))); + $field->expects($this->once()) + ->method('addError') + ->with($this->equalTo('Message'), $this->equalTo($expectedPath), $this->equalTo(FieldGroup::DATA_ERROR)); + + $group = new FieldGroup('author'); + $group->add($field); + + $group->addError('Message', new PropertyPath('address.street'), FieldGroup::DATA_ERROR); + } + + public function testAddErrorMapsErrorsOntoFieldsInAnonymousGroups() + { + // path is expected to point at "address" + $expectedPath = new PropertyPath('address'); + + $field = $this->createMockField('address'); + $field->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue(new PropertyPath('address'))); + $field->expects($this->once()) + ->method('addError') + ->with($this->equalTo('Message'), $this->equalTo($expectedPath), $this->equalTo(FieldGroup::DATA_ERROR)); + + $group = new FieldGroup('author'); + $group2 = new FieldGroup('anonymous', array('property_path' => null)); + $group2->add($field); + $group->add($group2); + + $group->addError('Message', new PropertyPath('address'), FieldGroup::DATA_ERROR); + } + + public function testAddThrowsExceptionIfAlreadyBound() + { + $group = new FieldGroup('author'); + $group->add($this->createMockField('firstName')); + $group->bind(array('firstName' => 'Bernhard')); + + $this->setExpectedException('Symfony\Components\Form\Exception\AlreadyBoundException'); + $group->add($this->createMockField('lastName')); + } + + public function testAddSetsFieldParent() + { + $group = new FieldGroup('author'); + + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('setParent'); + // PHPUnit fails to compare infinitely recursive objects + //->with($this->equalTo($group)); + + $group->add($field); + } + + public function testRemoveUnsetsFieldParent() + { + $group = new FieldGroup('author'); + + $field = $this->createMockField('firstName'); + $field->expects($this->exactly(2)) + ->method('setParent'); + // PHPUnit fails to compare subsequent method calls with different arguments + + $group->add($field); + $group->remove('firstName'); + } + + public function testMergeAddsFieldsFromAnotherGroup() + { + $group1 = new FieldGroup('author'); + $group1->add($field1 = new TestField('firstName')); + + $group2 = new FieldGroup('publisher'); + $group2->add($field2 = new TestField('lastName')); + + $group1->merge($group2); + + $this->assertTrue($group1->has('lastName')); + $this->assertEquals(new PropertyPath('publisher.lastName'), $group1->get('lastName')->getPropertyPath()); + } + + public function testMergeThrowsExceptionIfOtherGroupAlreadyBound() + { + $group1 = new FieldGroup('author'); + $group2 = new FieldGroup('publisher'); + $group2->add($this->createMockField('firstName')); + + $group2->bind(array('firstName' => 'Bernhard')); + + $this->setExpectedException('Symfony\Components\Form\Exception\AlreadyBoundException'); + $group1->merge($group2); + } + + public function testAddUpdatesFieldFromTransformedData() + { + $originalAuthor = new Author(); + $transformedAuthor = new Author(); + // the authors should differ to make sure the test works + $transformedAuthor->firstName = 'Foo'; + + $group = new FieldGroup('author'); + + $transformer = $this->createMockTransformer(); + $transformer->expects($this->once()) + ->method('transform') + ->with($this->equalTo($originalAuthor)) + ->will($this->returnValue($transformedAuthor)); + + $group->setValueTransformer($transformer); + $group->setData($originalAuthor); + + $field = $this->createMockField('firstName'); + $field->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue(new PropertyPath('firstName'))); + $field->expects($this->once()) + ->method('updateFromObject') + ->with($this->equalTo($transformedAuthor)); + + $group->add($field); + } + + public function testAddDoesNotUpdateFieldsWithEmptyPropertyPath() + { + $group = new FieldGroup('author'); + $group->setData(new Author()); + + $field = $this->createMockField('firstName'); + $field->expects($this->any()) + ->method('getPropertyPath') + ->will($this->returnValue(null)); + $field->expects($this->never()) + ->method('updateFromObject'); + + $group->add($field); + } + + public function testAddDoesNotUpdateFieldIfTransformedDataIsEmpty() + { + $originalAuthor = new Author(); + + $group = new FieldGroup('author'); + + $transformer = $this->createMockTransformer(); + $transformer->expects($this->once()) + ->method('transform') + ->with($this->equalTo($originalAuthor)) + ->will($this->returnValue('')); + + $group->setValueTransformer($transformer); + $group->setData($originalAuthor); + + $field = $this->createMockField('firstName'); + $field->expects($this->never()) + ->method('updateFromObject'); + + $group->add($field); + } + + public function testSetDataUpdatesAllFieldsFromTransformedData() + { + $originalAuthor = new Author(); + $transformedAuthor = new Author(); + // the authors should differ to make sure the test works + $transformedAuthor->firstName = 'Foo'; + + $group = new FieldGroup('author'); + + $transformer = $this->createMockTransformer(); + $transformer->expects($this->once()) + ->method('transform') + ->with($this->equalTo($originalAuthor)) + ->will($this->returnValue($transformedAuthor)); + + $group->setValueTransformer($transformer); + + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('updateFromObject') + ->with($this->equalTo($transformedAuthor)); + + $group->add($field); + + $field = $this->createMockField('lastName'); + $field->expects($this->once()) + ->method('updateFromObject') + ->with($this->equalTo($transformedAuthor)); + + $group->add($field); + + $group->setData($originalAuthor); + } + + public function testSetDataThrowsAnExceptionIfArgumentIsNotObjectOrArray() + { + $group = new FieldGroup('author'); + + $this->setExpectedException('InvalidArgumentException'); + + $group->setData('foobar'); + } + + public function testBindUpdatesTransformedDataFromAllFields() + { + $originalAuthor = new Author(); + $transformedAuthor = new Author(); + // the authors should differ to make sure the test works + $transformedAuthor->firstName = 'Foo'; + + $group = new FieldGroup('author'); + + $transformer = $this->createMockTransformer(); + $transformer->expects($this->once()) + ->method('transform') + ->with($this->equalTo($originalAuthor)) + ->will($this->returnValue($transformedAuthor)); + + $group->setValueTransformer($transformer); + $group->setData($originalAuthor); + + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('updateObject') + ->with($this->equalTo($transformedAuthor)); + + $group->add($field); + + $field = $this->createMockField('lastName'); + $field->expects($this->once()) + ->method('updateObject') + ->with($this->equalTo($transformedAuthor)); + + $group->add($field); + + $group->bind(array()); // irrelevant + } + + public function testGetDataReturnsObject() + { + $group = new FieldGroup('author'); + $object = new \stdClass(); + $group->setData($object); + $this->assertEquals($object, $group->getData()); + } + + public function testGetDisplayedDataForwardsCall() + { + $field = $this->createValidMockField('firstName'); + $field->expects($this->atLeastOnce()) + ->method('getDisplayedData') + ->will($this->returnValue('Bernhard')); + + $group = new FieldGroup('author'); + $group->add($field); + + $this->assertEquals(array('firstName' => 'Bernhard'), $group->getDisplayedData()); + } + + public function testIsMultipartIfAnyFieldIsMultipart() + { + $group = new FieldGroup('author'); + $group->add($this->createMultipartMockField('firstName')); + $group->add($this->createNonMultipartMockField('lastName')); + + $this->assertTrue($group->isMultipart()); + } + + public function testIsNotMultipartIfNoFieldIsMultipart() + { + $group = new FieldGroup('author'); + $group->add($this->createNonMultipartMockField('firstName')); + $group->add($this->createNonMultipartMockField('lastName')); + + $this->assertFalse($group->isMultipart()); + } + + public function testRenderForwardsToRenderer() + { + $group = new FieldGroup('author'); + + $renderer = $this->createMockRenderer(); + $renderer->expects($this->once()) + ->method('render') + ->with($this->equalTo($group), $this->equalTo(array('foo' => 'bar'))) + ->will($this->returnValue('HTML')); + + $group->setRenderer($renderer); + + // test + $output = $group->render(array('foo' => 'bar')); + + $this->assertEquals('HTML', $output); + } + + public function testRenderErrorsForwardsToRenderer() + { + $group = new FieldGroup('author'); + + $renderer = $this->createMockRenderer(); + $renderer->expects($this->once()) + ->method('renderErrors') + ->with($this->equalTo($group)) + ->will($this->returnValue('HTML')); + + $group->setRenderer($renderer); + + // test + $output = $group->renderErrors(); + + $this->assertEquals('HTML', $output); + } + + public function testLocaleIsPassedToRenderer() + { + $renderer = $this->getMock('Symfony\Components\Form\Renderer\RendererInterface'); + $renderer->expects($this->once()) + ->method('setLocale') + ->with($this->equalTo('de_DE')); + + $group = new FieldGroup('author'); + $group->setRenderer($renderer); + $group->setLocale('de_DE'); + $group->render(); + } + + public function testTranslatorIsPassedToRenderer() + { + $translator = $this->getMock('Symfony\Components\I18N\TranslatorInterface'); + $renderer = $this->getMock('Symfony\Components\Form\Renderer\RendererInterface'); + $renderer->expects($this->once()) + ->method('setTranslator') + ->with($this->equalTo($translator)); + + $group = new FieldGroup('author'); + $group->setRenderer($renderer); + $group->setTranslator($translator); + $group->render(); + } + + public function testTranslatorIsNotPassedToRendererIfNotSet() + { + $renderer = $this->getMock('Symfony\Components\Form\Renderer\RendererInterface'); + $renderer->expects($this->never()) + ->method('setTranslator'); + + $group = new FieldGroup('author'); + $group->setRenderer($renderer); + $group->render(); + } + + public function testLocaleIsPassedToField_SetBeforeAddingTheField() + { + $field = $this->getMock('Symfony\Components\Form\Field', array(), array(), '', false, false); + $field->expects($this->any()) + ->method('getKey') + ->will($this->returnValue('firstName')); + $field->expects($this->once()) + ->method('setLocale') + ->with($this->equalTo('de_DE')); + + $group = new FieldGroup('author'); + $group->setLocale('de_DE'); + $group->add($field); + } + + public function testLocaleIsPassedToField_SetAfterAddingTheField() + { + $field = $this->getMockForAbstractClass(__NAMESPACE__ . '\FieldGroupTest_Field', array(), '', false, false); + $field->expects($this->any()) + ->method('getKey') + ->will($this->returnValue('firstName')); +// DOESN'T WORK! +// $field = $this->getMock(__NAMESPACE__ . '\Fixtures\Field', array(), array(), '', false, false); +// $field->expects($this->once()) +// ->method('setLocale') +// ->with($this->equalTo('de_AT')); +// $field->expects($this->once()) +// ->method('setLocale') +// ->with($this->equalTo('de_DE')); + + $group = new FieldGroup('author'); + $group->add($field); + $group->setLocale('de_DE'); + + $this->assertEquals(array(\Locale::getDefault(), 'de_DE'), $field->locales); + } + + public function testTranslatorIsPassedToField_SetBeforeAddingTheField() + { + $translator = $this->getMock('Symfony\Components\I18N\TranslatorInterface'); + $field = $this->getMock('Symfony\Components\Form\Field', array(), array(), '', false, false); + $field->expects($this->any()) + ->method('getKey') + ->will($this->returnValue('firstName')); + $field->expects($this->once()) + ->method('setTranslator') + ->with($this->equalTo($translator)); + + $group = new FieldGroup('author'); + $group->setTranslator($translator); + $group->add($field); + } + + public function testTranslatorIsPassedToField_SetAfterAddingTheField() + { + $translator = $this->getMock('Symfony\Components\I18N\TranslatorInterface'); + $field = $this->getMock('Symfony\Components\Form\Field', array(), array(), '', false, false); + $field->expects($this->any()) + ->method('getKey') + ->will($this->returnValue('firstName')); + $field->expects($this->once()) + ->method('setTranslator') + ->with($this->equalTo($translator)); + + $group = new FieldGroup('author'); + $group->add($field); + $group->setTranslator($translator); + } + + public function testTranslatorIsNotPassedToFieldIfNotSet() + { + $field = $this->getMock('Symfony\Components\Form\Field', array(), array(), '', false, false); + $field->expects($this->any()) + ->method('getKey') + ->will($this->returnValue('firstName')); + $field->expects($this->never()) + ->method('setTranslator'); + + $group = new FieldGroup('author'); + $group->add($field); + } + + public function testSupportsClone() + { + $group = new FieldGroup('author'); + $group->add($this->createMockField('firstName')); + + $clone = clone $group; + + $this->assertNotSame($clone['firstName'], $group['firstName']); + } + + public function testBindWithoutPriorSetData() + { + return; // TODO + $field = $this->createMockField('firstName'); + $field->expects($this->any()) + ->method('getData') + ->will($this->returnValue('Bernhard')); + + $group = new FieldGroup('author'); + $group->add($field); + + $group->bind(array('firstName' => 'Bernhard')); + + $this->assertEquals(array('firstName' => 'Bernhard'), $group->getData()); + } + + public function testSetGenerator_calledBeforeAdding() + { + $generator = $this->getMock('Symfony\Components\Form\HtmlGeneratorInterface'); + + $field = $this->createMockField('firstName'); + $field->expects($this->once()) + ->method('setGenerator') + ->with($this->equalTo($generator)); + + $group = new FieldGroup('author'); + $group->setGenerator($generator); + $group->add($field); + } + + public function testSetGenerator_calledAfterAdding() + { + $generator = $this->getMock('Symfony\Components\Form\HtmlGeneratorInterface'); + + $field = $this->createMockField('firstName'); + $field->expects($this->exactly(2)) // cannot test different arguments :( + ->method('setGenerator'); + + $group = new FieldGroup('author'); + $group->add($field); + $group->setGenerator($generator); + } + + protected function createMockField($key) + { + $field = $this->getMock( + 'Symfony\Components\Form\FieldInterface', + array(), + array(), + '', + false, // don't use constructor + false // don't call parent::__clone + ); + + $field->expects($this->any()) + ->method('getKey') + ->will($this->returnValue($key)); + + return $field; + } + + protected function createInvalidMockField($key) + { + $field = $this->createMockField($key); + $field->expects($this->any()) + ->method('isValid') + ->will($this->returnValue(false)); + + return $field; + } + + protected function createValidMockField($key) + { + $field = $this->createMockField($key); + $field->expects($this->any()) + ->method('isValid') + ->will($this->returnValue(true)); + + return $field; + } + + protected function createNonMultipartMockField($key) + { + $field = $this->createMockField($key); + $field->expects($this->any()) + ->method('isMultipart') + ->will($this->returnValue(false)); + + return $field; + } + + protected function createMultipartMockField($key) + { + $field = $this->createMockField($key); + $field->expects($this->any()) + ->method('isMultipart') + ->will($this->returnValue(true)); + + return $field; + } + + protected function createMockRenderer() + { + return $this->getMock('Symfony\Components\Form\Renderer\RendererInterface'); + } + + protected function createMockTransformer() + { + return $this->getMock('Symfony\Components\Form\ValueTransformer\ValueTransformerInterface', array(), array(), '', false, false); + } +} diff --git a/tests/Symfony/Tests/Components/Form/FieldTest.php b/tests/Symfony/Tests/Components/Form/FieldTest.php new file mode 100644 index 000000000000..618d98961c00 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/FieldTest.php @@ -0,0 +1,666 @@ +field = new TestField('title'); + } + + public function testPassRequiredAsOption() + { + $field = new TestField('title', array('required' => false)); + + $this->assertFalse($field->isRequired()); + + $field = new TestField('title', array('required' => true)); + + $this->assertTrue($field->isRequired()); + } + + public function testPassDisabledAsOption() + { + $field = new TestField('title', array('disabled' => false)); + + $this->assertFalse($field->isDisabled()); + + $field = new TestField('title', array('disabled' => true)); + + $this->assertTrue($field->isDisabled()); + } + + public function testFieldIsDisabledIfParentIsDisabled() + { + $field = new TestField('title', array('disabled' => false)); + $field->setParent(new TestField('title', array('disabled' => true))); + + $this->assertTrue($field->isDisabled()); + } + + public function testFieldWithNoErrorsIsValid() + { + $this->field->bind('data'); + + $this->assertTrue($this->field->isValid()); + } + + public function testFieldWithErrorsIsInvalid() + { + $this->field->bind('data'); + $this->field->addError('Some error'); + + $this->assertFalse($this->field->isValid()); + } + + public function testBindResetsErrors() + { + $this->field->addError('Some error'); + $this->field->bind('data'); + + $this->assertTrue($this->field->isValid()); + } + + public function testUnboundFieldIsInvalid() + { + $this->assertFalse($this->field->isValid()); + } + + public function testGetNameReturnsKey() + { + $this->assertEquals('title', $this->field->getName()); + } + + public function testGetNameIncludesParent() + { + $this->field->setParent($this->createMockGroupWithName('news[article]')); + + $this->assertEquals('news[article][title]', $this->field->getName()); + } + + public function testGetIdReturnsKey() + { + $this->assertEquals('title', $this->field->getId()); + } + + public function testGetIdIncludesParent() + { + $this->field->setParent($this->createMockGroupWithId('news_article')); + + $this->assertEquals('news_article_title', $this->field->getId()); + } + + public function testLocaleIsPassedToLocalizableValueTransformer_setLocaleCalledBefore() + { + $transformer = $this->getMock('Symfony\Components\Form\ValueTransformer\ValueTransformerInterface'); + $transformer->expects($this->once()) + ->method('setLocale') + ->with($this->equalTo('de_DE')); + + $this->field->setLocale('de_DE'); + $this->field->setValueTransformer($transformer); + } + + public function testLocaleIsPassedToValueTransformer_setLocaleCalledAfter() + { + $transformer = $this->getMock('Symfony\Components\Form\ValueTransformer\ValueTransformerInterface'); + $transformer->expects($this->exactly(2)) + ->method('setLocale'); // we can't test the params cause they differ :( + + $this->field->setValueTransformer($transformer); + $this->field->setLocale('de_DE'); + } + + public function testIsRequiredReturnsOwnValueIfNoParent() + { + $this->field->setRequired(true); + $this->assertTrue($this->field->isRequired()); + + $this->field->setRequired(false); + $this->assertFalse($this->field->isRequired()); + } + + public function testIsRequiredReturnsOwnValueIfParentIsRequired() + { + $group = $this->createMockGroup(); + $group->expects($this->any()) + ->method('isRequired') + ->will($this->returnValue(true)); + + $this->field->setParent($group); + + $this->field->setRequired(true); + $this->assertTrue($this->field->isRequired()); + + $this->field->setRequired(false); + $this->assertFalse($this->field->isRequired()); + } + + public function testIsRequiredReturnsFalseIfParentIsNotRequired() + { + $group = $this->createMockGroup(); + $group->expects($this->any()) + ->method('isRequired') + ->will($this->returnValue(false)); + + $this->field->setParent($group); + $this->field->setRequired(true); + + $this->assertFalse($this->field->isRequired()); + } + + public function testExceptionIfUnknownOption() + { + $this->setExpectedException('Symfony\Components\Form\Exception\InvalidOptionsException'); + + new RequiredOptionsField('name', array('bar' => 'baz', 'moo' => 'maa')); + } + + public function testExceptionIfMissingOption() + { + $this->setExpectedException('Symfony\Components\Form\Exception\MissingOptionsException'); + + new RequiredOptionsField('name'); + } + + public function testIsBound() + { + $this->assertFalse($this->field->isBound()); + $this->field->bind('symfony'); + $this->assertTrue($this->field->isBound()); + } + + public function testDefaultValuesAreTransformedCorrectly() + { + $field = new TestField('name'); + + $this->assertEquals(null, $this->field->getData()); + $this->assertEquals('', $this->field->getDisplayedData()); + } + + public function testValuesAreTransformedCorrectlyIfNull() + { + // The value is converted to an empty string and NOT passed to the + // value transformer + $transformer = $this->createMockTransformer(); + $transformer->expects($this->never()) + ->method('transform'); + + $this->field->setValueTransformer($transformer); + $this->field->setData(null); + + $this->assertSame(null, $this->field->getData()); + $this->assertSame('', $this->field->getDisplayedData()); + } + + public function testValuesAreTransformedCorrectlyIfNull_noValueTransformer() + { + $this->field->setData(null); + + $this->assertSame(null, $this->field->getData()); + $this->assertSame('', $this->field->getDisplayedData()); + } + + public function testBoundValuesAreTransformedCorrectly() + { + $field = $this->getMock( + 'Symfony\Tests\Components\Form\Fixtures\TestField', + array('processData'), // only mock processData() + array('title') + ); + + // 1. The value is converted to a string and passed to the value transformer + $transformer = $this->createMockTransformer(); + $transformer->expects($this->once()) + ->method('reverseTransform') + ->with($this->identicalTo('0')) + ->will($this->returnValue('reverse[0]')); + + $field->setValueTransformer($transformer); + + // 2. The output of the reverse transformation is passed to processData() + $field->expects($this->once()) + ->method('processData') + ->with($this->equalTo('reverse[0]')) + ->will($this->returnValue('processed[reverse[0]]')); + + // 3. The processed data is transformed again (for displayed data) + $transformer->expects($this->once()) + ->method('transform') + ->with($this->equalTo('processed[reverse[0]]')) + ->will($this->returnValue('transform[processed[reverse[0]]]')); + + $field->bind(0); + + $this->assertEquals('processed[reverse[0]]', $field->getData()); + $this->assertEquals('transform[processed[reverse[0]]]', $field->getDisplayedData()); + } + + public function testBoundValuesAreTransformedCorrectlyIfEmpty_processDataReturnsValue() + { + $field = $this->getMock( + 'Symfony\Tests\Components\Form\Fixtures\TestField', + array('processData'), // only mock processData() + array('title') + ); + + // 1. Empty values are always converted to NULL. They are never passed to + // the value transformer + $transformer = $this->createMockTransformer(); + $transformer->expects($this->never()) + ->method('reverseTransform'); + + $field->setValueTransformer($transformer); + + // 2. NULL is passed to processData() + $field->expects($this->once()) + ->method('processData') + ->with($this->identicalTo(null)) + ->will($this->returnValue('processed')); + + // 3. The processed data is transformed (for displayed data) + $transformer->expects($this->once()) + ->method('transform') + ->with($this->equalTo('processed')) + ->will($this->returnValue('transform[processed]')); + + $field->bind(''); + + $this->assertSame('processed', $field->getData()); + $this->assertEquals('transform[processed]', $field->getDisplayedData()); + } + + public function testBoundValuesAreTransformedCorrectlyIfEmpty_processDataReturnsNull() + { + // 1. Empty values are always converted to NULL. They are never passed to + // the value transformer + $transformer = $this->createMockTransformer(); + $transformer->expects($this->never()) + ->method('reverseTransform'); + + $this->field->setValueTransformer($transformer); + + // 2. The processed data is NULL and therefore transformed to an empty + // string. It is NOT passed to the value transformer + $transformer->expects($this->never()) + ->method('transform'); + + $this->field->bind(''); + + $this->assertSame(null, $this->field->getData()); + $this->assertEquals('', $this->field->getDisplayedData()); + } + + public function testBoundValuesAreTransformedCorrectlyIfEmpty_processDataReturnsNull_noValueTransformer() + { + $this->field->bind(''); + + $this->assertSame(null, $this->field->getData()); + $this->assertEquals('', $this->field->getDisplayedData()); + } + + public function testValuesAreTransformedCorrectly() + { + // The value is passed to the value transformer + $transformer = $this->createMockTransformer(); + $transformer->expects($this->once()) + ->method('transform') + ->with($this->identicalTo(0)) + ->will($this->returnValue('transform[0]')); + + $this->field->setValueTransformer($transformer); + $this->field->setData(0); + + $this->assertEquals(0, $this->field->getData()); + $this->assertEquals('transform[0]', $this->field->getDisplayedData()); + } + + public function testBoundValuesAreTrimmedBeforeTransforming() + { + // The value is passed to the value transformer + $transformer = $this->createMockTransformer(); + $transformer->expects($this->once()) + ->method('reverseTransform') + ->with($this->identicalTo('a')) + ->will($this->returnValue('reverse[a]')); + + $transformer->expects($this->once()) + ->method('transform') + ->with($this->identicalTo('reverse[a]')) + ->will($this->returnValue('a')); + + $this->field->setValueTransformer($transformer); + $this->field->bind(' a '); + + $this->assertEquals('a', $this->field->getDisplayedData()); + $this->assertEquals('reverse[a]', $this->field->getData()); + } + + public function testBoundValuesAreNotTrimmedBeforeTransformingIfDisabled() + { + // The value is passed to the value transformer + $transformer = $this->createMockTransformer(); + $transformer->expects($this->once()) + ->method('reverseTransform') + ->with($this->identicalTo(' a ')) + ->will($this->returnValue('reverse[ a ]')); + + $transformer->expects($this->once()) + ->method('transform') + ->with($this->identicalTo('reverse[ a ]')) + ->will($this->returnValue(' a ')); + + $field = new TestField('title', array('trim' => false)); + $field->setValueTransformer($transformer); + $field->bind(' a '); + + $this->assertEquals(' a ', $field->getDisplayedData()); + $this->assertEquals('reverse[ a ]', $field->getData()); + } + + public function testUpdateFromObjectReadsArray() + { + $array = array('firstName' => 'Bernhard'); + + $field = new TestField('firstName'); + $field->updateFromObject($array); + + $this->assertEquals('Bernhard', $field->getData()); + } + + public function testUpdateFromObjectReadsArrayWithCustomPropertyPath() + { + $array = array('child' => array('index' => array('firstName' => 'Bernhard'))); + + $field = new TestField('firstName', array('property_path' => 'child[index].firstName')); + $field->updateFromObject($array); + + $this->assertEquals('Bernhard', $field->getData()); + } + + public function testUpdateFromObjectReadsProperty() + { + $object = new Author(); + $object->firstName = 'Bernhard'; + + $field = new TestField('firstName'); + $field->updateFromObject($object); + + $this->assertEquals('Bernhard', $field->getData()); + } + + public function testUpdateFromObjectReadsPropertyWithCustomPropertyPath() + { + $object = new Author(); + $object->child = array(); + $object->child['index'] = new Author(); + $object->child['index']->firstName = 'Bernhard'; + + $field = new TestField('firstName', array('property_path' => 'child[index].firstName')); + $field->updateFromObject($object); + + $this->assertEquals('Bernhard', $field->getData()); + } + + public function testUpdateFromObjectReadsArrayAccess() + { + $object = new \ArrayObject(); + $object['firstName'] = 'Bernhard'; + + $field = new TestField('firstName', array('property_path' => '[firstName]')); + $field->updateFromObject($object); + + $this->assertEquals('Bernhard', $field->getData()); + } + + public function testUpdateFromObjectThrowsExceptionIfArrayAccessExpected() + { + $field = new TestField('firstName', array('property_path' => '[firstName]')); + + $this->setExpectedException('Symfony\Components\Form\Exception\InvalidPropertyException'); + $field->updateFromObject(new Author()); + } + + /* + * The use case of this test is a field group with an empty property path. + * Even if the field group itself is not associated to a specific property, + * nested fields might be. + */ + public function testUpdateFromObjectPassesObjectThroughIfPropertyPathIsEmpty() + { + $object = new Author(); + $object->firstName = 'Bernhard'; + + $field = new TestField('firstName', array('property_path' => null)); + $field->updateFromObject($object); + + $this->assertEquals($object, $field->getData()); + } + + public function testUpdateFromObjectThrowsExceptionIfPropertyIsNotPublic() + { + $field = new TestField('privateProperty'); + + $this->setExpectedException('Symfony\Components\Form\Exception\PropertyAccessDeniedException'); + $field->updateFromObject(new Author()); + } + + public function testUpdateFromObjectReadsGetters() + { + $object = new Author(); + $object->setLastName('Schussek'); + + $field = new TestField('lastName'); + $field->updateFromObject($object); + + $this->assertEquals('Schussek', $field->getData()); + } + + public function testUpdateFromObjectThrowsExceptionIfGetterIsNotPublic() + { + $field = new TestField('privateGetter'); + + $this->setExpectedException('Symfony\Components\Form\Exception\PropertyAccessDeniedException'); + $field->updateFromObject(new Author()); + } + + public function testUpdateFromObjectReadsIssers() + { + $object = new Author(); + $object->setAustralian(false); + + $field = new TestField('australian'); + $field->updateFromObject($object); + + $this->assertSame(false, $field->getData()); + } + + public function testUpdateFromObjectThrowsExceptionIfIsserIsNotPublic() + { + $field = new TestField('privateIsser'); + + $this->setExpectedException('Symfony\Components\Form\Exception\PropertyAccessDeniedException'); + $field->updateFromObject(new Author()); + } + + public function testUpdateFromObjectThrowsExceptionIfPropertyDoesNotExist() + { + $field = new TestField('foobar'); + + $this->setExpectedException('Symfony\Components\Form\Exception\InvalidPropertyException'); + $field->updateFromObject(new Author()); + } + + public function testUpdateObjectUpdatesArrays() + { + $array = array(); + + $field = new TestField('firstName'); + $field->bind('Bernhard'); + $field->updateObject($array); + + $this->assertEquals(array('firstName' => 'Bernhard'), $array); + } + + public function testUpdateObjectUpdatesArraysWithCustomPropertyPath() + { + $array = array(); + + $field = new TestField('firstName', array('property_path' => 'child[index].firstName')); + $field->bind('Bernhard'); + $field->updateObject($array); + + $this->assertEquals(array('child' => array('index' => array('firstName' => 'Bernhard'))), $array); + } + + /* + * This is important so that bind() can work even if setData() was not called + * before + */ + public function testUpdateObjectTreatsEmptyValuesAsArrays() + { + $array = null; + + $field = new TestField('firstName'); + $field->bind('Bernhard'); + $field->updateObject($array); + + $this->assertEquals(array('firstName' => 'Bernhard'), $array); + } + + public function testUpdateObjectUpdatesProperties() + { + $object = new Author(); + + $field = new TestField('firstName'); + $field->bind('Bernhard'); + $field->updateObject($object); + + $this->assertEquals('Bernhard', $object->firstName); + } + + public function testUpdateObjectUpdatesPropertiesWithCustomPropertyPath() + { + $object = new Author(); + $object->child = array(); + $object->child['index'] = new Author(); + + $field = new TestField('firstName', array('property_path' => 'child[index].firstName')); + $field->bind('Bernhard'); + $field->updateObject($object); + + $this->assertEquals('Bernhard', $object->child['index']->firstName); + } + + public function testUpdateObjectUpdatesArrayAccess() + { + $object = new \ArrayObject(); + + $field = new TestField('firstName', array('property_path' => '[firstName]')); + $field->bind('Bernhard'); + $field->updateObject($object); + + $this->assertEquals('Bernhard', $object['firstName']); + } + + public function testUpdateObjectThrowsExceptionIfArrayAccessExpected() + { + $field = new TestField('firstName', array('property_path' => '[firstName]')); + $field->bind('Bernhard'); + + $this->setExpectedException('Symfony\Components\Form\Exception\InvalidPropertyException'); + $field->updateObject(new Author()); + } + + public function testUpdateObjectDoesNotUpdatePropertyIfPropertyPathIsEmpty() + { + $object = new Author(); + + $field = new TestField('firstName', array('property_path' => null)); + $field->bind('Bernhard'); + $field->updateObject($object); + + $this->assertEquals(null, $object->firstName); + } + + public function testUpdateObjectUpdatesSetters() + { + $object = new Author(); + + $field = new TestField('lastName'); + $field->bind('Schussek'); + $field->updateObject($object); + + $this->assertEquals('Schussek', $object->getLastName()); + } + + public function testUpdateObjectThrowsExceptionIfGetterIsNotPublic() + { + $field = new TestField('privateSetter'); + $field->bind('foobar'); + + $this->setExpectedException('Symfony\Components\Form\Exception\PropertyAccessDeniedException'); + $field->updateObject(new Author()); + } + + protected function createMockTransformer() + { + return $this->getMock('Symfony\Components\Form\ValueTransformer\ValueTransformerInterface', array(), array(), '', false, false); + } + + protected function createMockTransformerTransformingTo($value) + { + $transformer = $this->createMockTransformer(); + $transformer->expects($this->any()) + ->method('reverseTransform') + ->will($this->returnValue($value)); + + return $transformer; + } + + protected function createMockGroup() + { + return $this->getMock( + 'Symfony\Components\Form\FieldGroup', + array(), + array(), + '', + false // don't call constructor + ); + } + + protected function createMockGroupWithName($name) + { + $group = $this->createMockGroup(); + $group->expects($this->any()) + ->method('getName') + ->will($this->returnValue($name)); + + return $group; + } + + protected function createMockGroupWithId($id) + { + $group = $this->createMockGroup(); + $group->expects($this->any()) + ->method('getId') + ->will($this->returnValue($id)); + + return $group; + } +} diff --git a/tests/Symfony/Tests/Components/Form/Fixtures/Author.php b/tests/Symfony/Tests/Components/Form/Fixtures/Author.php new file mode 100644 index 000000000000..63e0601251df --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/Fixtures/Author.php @@ -0,0 +1,51 @@ +lastName = $lastName; + } + + public function getLastName() + { + return $this->lastName; + } + + private function getPrivateGetter() + { + return 'foobar'; + } + + public function setAustralian($australian) + { + $this->australian = $australian; + } + + public function isAustralian() + { + return $this->australian; + } + + private function isPrivateIsser() + { + return true; + } + + public function getPrivateSetter() + { + } + + private function setPrivateSetter($data) + { + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/Fixtures/InvalidField.php b/tests/Symfony/Tests/Components/Form/Fixtures/InvalidField.php new file mode 100644 index 000000000000..aba4b0a2a5e5 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/Fixtures/InvalidField.php @@ -0,0 +1,17 @@ +addOption('foo'); + $this->addRequiredOption('bar'); + } + + public function render(array $attributes = array()) + { + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/Fixtures/TestField.php b/tests/Symfony/Tests/Components/Form/Fixtures/TestField.php new file mode 100644 index 000000000000..dc247a5f40ca --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/Fixtures/TestField.php @@ -0,0 +1,12 @@ +add(new Field('firstName')); + } +} + +class FormTest extends \PHPUnit_Framework_TestCase +{ + protected $validator; + protected $form; + + protected function setUp() + { + Form::disableDefaultCsrfProtection(); + Form::setDefaultCsrfSecret(null); + $this->validator = $this->createMockValidator(); + $this->form = new Form('author', new Author(), $this->validator); + } + + public function testConstructInitializesObject() + { + $this->assertEquals(new Author(), $this->form->getData()); + } + + public function testIsCsrfProtected() + { + $this->assertFalse($this->form->isCsrfProtected()); + + $this->form->enableCsrfProtection(); + + $this->assertTrue($this->form->isCsrfProtected()); + + $this->form->disableCsrfProtection(); + + $this->assertFalse($this->form->isCsrfProtected()); + } + + public function testNoCsrfProtectionByDefault() + { + $form = new Form('author', new Author(), $this->validator); + + $this->assertFalse($form->isCsrfProtected()); + } + + public function testDefaultCsrfProtectionCanBeEnabled() + { + Form::enableDefaultCsrfProtection(); + $form = new Form('author', new Author(), $this->validator); + + $this->assertTrue($form->isCsrfProtected()); + } + + public function testGeneratedCsrfSecretByDefault() + { + $form = new Form('author', new Author(), $this->validator); + + $this->assertTrue(strlen($form->getCsrfSecret()) >= 32); + } + + public function testDefaultCsrfSecretCanBeSet() + { + Form::setDefaultCsrfSecret('foobar'); + $form = new Form('author', new Author(), $this->validator); + + $this->assertEquals('foobar', $form->getCsrfSecret()); + } + + public function testDefaultCsrfFieldNameCanBeSet() + { + Form::setDefaultCsrfFieldName('foobar'); + $form = new Form('author', new Author(), $this->validator); + + $this->assertEquals('foobar', $form->getCsrfFieldName()); + } + + public function testCsrfProtectedFormsHaveExtraField() + { + $this->form->enableCsrfProtection(); + + $this->assertTrue($this->form->has($this->form->getCsrfFieldName())); + + $field = $this->form->get($this->form->getCsrfFieldName()); + + $this->assertTrue($field instanceof HiddenField); + $this->assertGreaterThanOrEqual(32, strlen($field->getDisplayedData())); + } + + public function testIsCsrfTokenValidPassesIfCsrfProtectionIsDisabled() + { + $this->form->bind(array()); + + $this->assertTrue($this->form->isCsrfTokenValid()); + } + + public function testIsCsrfTokenValidPasses() + { + $this->form->enableCsrfProtection(); + + $field = $this->form->getCsrfFieldName(); + $token = $this->form->get($field)->getDisplayedData(); + + $this->form->bind(array($field => $token)); + + $this->assertTrue($this->form->isCsrfTokenValid()); + } + + public function testIsCsrfTokenValidFails() + { + $this->form->enableCsrfProtection(); + + $field = $this->form->getCsrfFieldName(); + + $this->form->bind(array($field => 'foobar')); + + $this->assertFalse($this->form->isCsrfTokenValid()); + } + + public function testDefaultLocaleCanBeSet() + { + Form::setDefaultLocale('de-DE-1996'); + $form = new Form('author', new Author(), $this->validator); + + $field = $this->getMock('Symfony\Components\Form\Field', array(), array(), '', false, false); + $field->expects($this->any()) + ->method('getKey') + ->will($this->returnValue('firstName')); + $field->expects($this->once()) + ->method('setLocale') + ->with($this->equalTo('de-DE-1996')); + + $form->add($field); + } + + public function testDefaultTranslatorCanBeSet() + { + $translator = $this->getMock('Symfony\Components\I18N\TranslatorInterface'); + Form::setDefaultTranslator($translator); + $form = new Form('author', new Author(), $this->validator); + + $field = $this->getMock('Symfony\Components\Form\Field', array(), array(), '', false, false); + $field->expects($this->any()) + ->method('getKey') + ->will($this->returnValue('firstName')); + $field->expects($this->once()) + ->method('setTranslator') + ->with($this->equalTo($translator)); + + $form->add($field); + } + + public function testValidationGroupsCanBeSet() + { + $form = new Form('author', new Author(), $this->validator); + + $this->assertNull($form->getValidationGroups()); + $form->setValidationGroups('group'); + $this->assertEquals(array('group'), $form->getValidationGroups()); + $form->setValidationGroups(array('group1', 'group2')); + $this->assertEquals(array('group1', 'group2'), $form->getValidationGroups()); + $form->setValidationGroups(null); + $this->assertNull($form->getValidationGroups()); + } + + public function testBindUsesValidationGroups() + { + $field = $this->createMockField('firstName'); + $form = new Form('author', new Author(), $this->validator); + $form->add($field); + $form->setValidationGroups('group'); + + $this->validator->expects($this->once()) + ->method('validate') + ->with($this->equalTo($form), $this->equalTo(array('group'))); + + $form->bind(array()); // irrelevant + } + + public function testBindConvertsUploadedFiles() + { + $tmpFile = $this->createTempFile(); + $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain', 100, 0); + + $field = $this->createMockField('file'); + $field->expects($this->once()) + ->method('bind') + ->with($this->equalTo($file)); + + $form = new Form('author', new Author(), $this->validator); + $form->add($field); + + // test + $form->bind(array(), array('file' => array( + 'name' => basename($tmpFile), + 'type' => 'text/plain', + 'tmp_name' => $tmpFile, + 'error' => 0, + 'size' => 100 + ))); + } + + public function testBindConvertsUploadedFilesWithPhpBug() + { + $tmpFile = $this->createTempFile(); + $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain', 100, 0); + + $field = $this->createMockField('file'); + $field->expects($this->once()) + ->method('bind') + ->with($this->equalTo($file)); + + $form = new Form('author', new Author(), $this->validator); + $form->add($field); + + // test + $form->bind(array(), array( + 'name' => array( + 'file' => basename($tmpFile), + ), + 'type' => array( + 'file' => 'text/plain', + ), + 'tmp_name' => array( + 'file' => $tmpFile, + ), + 'error' => array( + 'file' => 0, + ), + 'size' => array( + 'file' => 100, + ), + )); + } + + public function testBindConvertsNestedUploadedFilesWithPhpBug() + { + $tmpFile = $this->createTempFile(); + $file = new UploadedFile($tmpFile, basename($tmpFile), 'text/plain', 100, 0); + + $group = $this->getMock( + 'Symfony\Components\Form\FieldGroup', + array('bind'), + array('child', array('property_path' => null)) + ); + $group->expects($this->once()) + ->method('bind') + ->with($this->equalTo(array('file' => $file))); + + $form = new Form('author', new Author(), $this->validator); + $form->add($group); + + // test + $form->bind(array(), array( + 'name' => array( + 'child' => array('file' => basename($tmpFile)), + ), + 'type' => array( + 'child' => array('file' => 'text/plain'), + ), + 'tmp_name' => array( + 'child' => array('file' => $tmpFile), + ), + 'error' => array( + 'child' => array('file' => 0), + ), + 'size' => array( + 'child' => array('file' => 100), + ), + )); + } + + public function testMultipartFormsWithoutParentsRequireFiles() + { + $form = new Form('author', new Author(), $this->validator); + $form->add($this->createMultipartMockField('file')); + + $this->setExpectedException('InvalidArgumentException'); + + // should be given in second argument + $form->bind(array('file' => 'test.txt')); + } + + public function testMultipartFormsWithParentsRequireNoFiles() + { + $form = new Form('author', new Author(), $this->validator); + $form->add($this->createMultipartMockField('file')); + + $form->setParent($this->createMockField('group')); + + // files are expected to be converted by the parent + $form->bind(array('file' => 'test.txt')); + } + + public function testRenderFormTagProducesValidXhtml() + { + $form = new Form('author', new Author(), $this->validator); + + $this->assertEquals('
', $form->renderFormTag('url')); + } + + public function testSetCharsetAdjustsGenerator() + { + $form = $this->getMock( + 'Symfony\Components\Form\Form', + array('setGenerator'), + array(), + '', + false // don't call original constructor + ); + + $form->expects($this->once()) + ->method('setGenerator') + ->with($this->equalTo(new HtmlGenerator('iso-8859-1'))); + + $form->setCharset('iso-8859-1'); + } + + protected function createMockField($key) + { + $field = $this->getMock( + 'Symfony\Components\Form\FieldInterface', + array(), + array(), + '', + false, // don't use constructor + false // don't call parent::__clone + ); + + $field->expects($this->any()) + ->method('getKey') + ->will($this->returnValue($key)); + + return $field; + } + + protected function createMockFieldGroup($key) + { + $field = $this->getMock( + 'Symfony\Components\Form\FieldGroup', + array(), + array(), + '', + false, // don't use constructor + false // don't call parent::__clone + ); + + $field->expects($this->any()) + ->method('getKey') + ->will($this->returnValue($key)); + + return $field; + } + + protected function createMultipartMockField($key) + { + $field = $this->createMockField($key); + $field->expects($this->any()) + ->method('isMultipart') + ->will($this->returnValue(true)); + + return $field; + } + + protected function createTempFile() + { + return tempnam(sys_get_temp_dir(), 'FormTest'); + } + + protected function createMockValidator() + { + return $this->getMock('Symfony\Components\Validator\ValidatorInterface'); + } +} diff --git a/tests/Symfony/Tests/Components/Form/HiddenFieldTest.php b/tests/Symfony/Tests/Components/Form/HiddenFieldTest.php new file mode 100644 index 000000000000..1b9650ede0d6 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/HiddenFieldTest.php @@ -0,0 +1,33 @@ +field = new HiddenField('name'); + } + + public function testRender() + { + $this->field->setData('foobar'); + + $html = ''; + + $this->assertEquals($html, $this->field->render(array( + 'class' => 'foobar', + ))); + } + + public function testIsHidden() + { + $this->assertTrue($this->field->isHidden()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/HtmlGeneratorTest.php b/tests/Symfony/Tests/Components/Form/HtmlGeneratorTest.php new file mode 100644 index 000000000000..31ad7c257c88 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/HtmlGeneratorTest.php @@ -0,0 +1,93 @@ +generator = new HtmlGenerator(); + } + + public function testEscape() + { + $this->assertEquals('<&abcd', $this->generator->escape('<&abcd')); + } + + public function testEscapeOnlyOnce() + { + $this->assertEquals('<&abcd', $this->generator->escape('<&abcd')); + } + + public function testAttribute() + { + $this->assertEquals('foo="bar"', $this->generator->attribute('foo', 'bar')); + } + + public function testEscapeAttribute() + { + $this->assertEquals('foo="<>"', $this->generator->attribute('foo', '<>')); + } + + public function testXhtmlAttribute() + { + HtmlGenerator::setXhtml(true); + $this->assertEquals('foo="foo"', $this->generator->attribute('foo', true)); + } + + public function testNonXhtmlAttribute() + { + HtmlGenerator::setXhtml(false); + $this->assertEquals('foo', $this->generator->attribute('foo', true)); + } + + public function testAttributes() + { + $html = $this->generator->attributes(array( + 'foo' => 'bar', + 'bar' => 'baz', + )); + $this->assertEquals(' foo="bar" bar="baz"', $html); + } + + public function testXhtmlTag() + { + HtmlGenerator::setXhtml(true); + $html = $this->generator->tag('input', array( + 'type' => 'text', + )); + $this->assertEquals('', $html); + } + + public function testNonXhtmlTag() + { + HtmlGenerator::setXhtml(false); + $html = $this->generator->tag('input', array( + 'type' => 'text', + )); + $this->assertEquals('', $html); + } + + public function testContentTag() + { + $html = $this->generator->contentTag('p', 'asdf', array( + 'class' => 'foo', + )); + $this->assertEquals('

asdf

', $html); + } + + // it should be possible to pass the output of the tag() method as body + // of the content tag + public function testDontEscapeContentTag() + { + $this->assertEquals('

<&

', $this->generator->contentTag('p', '<&')); + } + +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/InputFieldTest.php b/tests/Symfony/Tests/Components/Form/InputFieldTest.php new file mode 100644 index 000000000000..1c45b571eee8 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/InputFieldTest.php @@ -0,0 +1,34 @@ +setData('foobar'); + + $html = ''; + + $this->assertEquals($html, $field->render(array( + 'class' => 'foobar', + ))); + } + + public function testRender_disabled() + { + $field = new TestInputField('name', array('disabled' => true)); + $field->setData('foobar'); + + $html = ''; + + $this->assertEquals($html, $field->render()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/IntegerFieldTest.php b/tests/Symfony/Tests/Components/Form/IntegerFieldTest.php new file mode 100644 index 000000000000..20c7e35fd7f5 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/IntegerFieldTest.php @@ -0,0 +1,20 @@ +bind('1.678'); + + $this->assertSame(1, $field->getData()); + $this->assertSame('1', $field->getDisplayedData()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/MoneyFieldTest.php b/tests/Symfony/Tests/Components/Form/MoneyFieldTest.php new file mode 100644 index 000000000000..3401dabbebd7 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/MoneyFieldTest.php @@ -0,0 +1,46 @@ +setLocale('de_AT'); + $field->setData(1234); + + $html = ''; + + $this->assertEquals($html, $field->render(array('class' => 'foobar'))); + } + + public function testRenderWithCurrency_afterWidget() + { + $field = new MoneyField('name', array('currency' => 'EUR')); + + $field->setLocale('de_DE'); + $field->setData(1234); + + $html = ' €'; + + $this->assertEquals($html, $field->render()); + } + + public function testRenderWithCurrency_beforeWidget() + { + $field = new MoneyField('name', array('currency' => 'EUR')); + + $field->setLocale('en_US'); + $field->setData(1234); + + $html = '€ '; + + $this->assertEquals($html, $field->render()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/NumberFieldTest.php b/tests/Symfony/Tests/Components/Form/NumberFieldTest.php new file mode 100644 index 000000000000..6e4aa7735726 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/NumberFieldTest.php @@ -0,0 +1,46 @@ +setLocale('de_AT'); + $field->setData(1234.5678); + + $html = ''; + + $this->assertEquals($html, $field->render(array('class' => 'foobar'))); + } + + public function testRenderWithPrecision() + { + $field = new NumberField('name', array('precision' => 4)); + + $field->setLocale('de_AT'); + $field->setData(1234.5678); + + $html = ''; + + $this->assertEquals($html, $field->render()); + } + + public function testRenderWithGrouping() + { + $field = new NumberField('name', array('grouping' => true)); + + $field->setLocale('de_AT'); + $field->setData(1234.5678); + + $html = ''; + + $this->assertEquals($html, $field->render()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/PasswordFieldTest.php b/tests/Symfony/Tests/Components/Form/PasswordFieldTest.php new file mode 100644 index 000000000000..e7444b6f2437 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/PasswordFieldTest.php @@ -0,0 +1,51 @@ +setData('asdf'); + + $html = ''; + + $this->assertEquals($html, $field->render(array('class' => 'foobar'))); + } + + // when the user made an error in the form, display the value in the field + public function testRenderAfterBinding() + { + $field = new PasswordField('name'); + $field->bind('asdf'); + + $html = ''; + + $this->assertEquals($html, $field->render()); + } + + public function testRenderNotAlwaysEmpty() + { + $field = new PasswordField('name', array('always_empty' => false)); + $field->setData('asdf'); + + $html = ''; + + $this->assertEquals($html, $field->render()); + } + + public function testRenderNotAlwaysEmptyAfterBinding() + { + $field = new PasswordField('name', array('always_empty' => false)); + $field->bind('asdf'); + + $html = ''; + + $this->assertEquals($html, $field->render()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/PercentFieldTest.php b/tests/Symfony/Tests/Components/Form/PercentFieldTest.php new file mode 100644 index 000000000000..f32c9642d97a --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/PercentFieldTest.php @@ -0,0 +1,46 @@ +setLocale('de_DE'); + $field->setData(0.12); + + $html = ' %'; + + $this->assertEquals($html, $field->render()); + } + + public function testRenderWithPrecision() + { + $field = new PercentField('name', array('precision' => 2)); + + $field->setLocale('de_DE'); + $field->setData(0.1234); + + $html = ' %'; + + $this->assertEquals($html, $field->render()); + } + + public function testRenderWithInteger() + { + $field = new PercentField('name', array('type' => 'integer')); + + $field->setLocale('de_DE'); + $field->setData(123); + + $html = ' %'; + + $this->assertEquals($html, $field->render()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/PropertyPathTest.php b/tests/Symfony/Tests/Components/Form/PropertyPathTest.php new file mode 100644 index 000000000000..3ee2a0695aba --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/PropertyPathTest.php @@ -0,0 +1,85 @@ +assertEquals('reference', $path->getCurrent()); + $this->assertTrue($path->hasNext()); + $this->assertTrue($path->isProperty()); + $this->assertFalse($path->isIndex()); + + $path->next(); + + $this->assertEquals('traversable', $path->getCurrent()); + $this->assertTrue($path->hasNext()); + $this->assertTrue($path->isProperty()); + $this->assertFalse($path->isIndex()); + + $path->next(); + + $this->assertEquals('index', $path->getCurrent()); + $this->assertTrue($path->hasNext()); + $this->assertFalse($path->isProperty()); + $this->assertTrue($path->isIndex()); + + $path->next(); + + $this->assertEquals('property', $path->getCurrent()); + $this->assertFalse($path->hasNext()); + $this->assertTrue($path->isProperty()); + $this->assertFalse($path->isIndex()); + } + + public function testToString() + { + $path = new PropertyPath('reference.traversable[index].property'); + + $this->assertEquals('reference.traversable[index].property', $path->__toString()); + } + + public function testInvalidPropertyPath_noDotBeforeProperty() + { + $this->setExpectedException('Symfony\Components\Form\Exception\InvalidPropertyPathException'); + + new PropertyPath('[index]property'); + } + + public function testInvalidPropertyPath_dotAtTheBeginning() + { + $this->setExpectedException('Symfony\Components\Form\Exception\InvalidPropertyPathException'); + + new PropertyPath('.property'); + } + + public function testInvalidPropertyPath_unexpectedCharacters() + { + $this->setExpectedException('Symfony\Components\Form\Exception\InvalidPropertyPathException'); + + new PropertyPath('property.$field'); + } + + public function testInvalidPropertyPath_empty() + { + $this->setExpectedException('Symfony\Components\Form\Exception\InvalidPropertyPathException'); + + new PropertyPath(''); + } + + public function testNextThrowsExceptionIfNoNextElement() + { + $path = new PropertyPath('property'); + + $this->setExpectedException('OutOfBoundsException'); + + $path->next(); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/RadioFieldTest.php b/tests/Symfony/Tests/Components/Form/RadioFieldTest.php new file mode 100644 index 000000000000..c769afc9da4c --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/RadioFieldTest.php @@ -0,0 +1,35 @@ +setData(true); + + $html = ''; + + $this->assertEquals($html, $field->render(array( + 'class' => 'foobar', + ))); + } + + // when a radio button is in a field group, all radio buttons in that group + // should have the same name + public function testRenderParentName() + { + $field = new RadioField('name'); + $field->setParent(new FieldGroup('parent')); + + $html = ''; + + $this->assertEquals($html, $field->render()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/Renderer/RendererTestCase.php b/tests/Symfony/Tests/Components/Form/Renderer/RendererTestCase.php new file mode 100644 index 000000000000..30a757195c0d --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/Renderer/RendererTestCase.php @@ -0,0 +1,23 @@ +getMock('Symfony\Components\Form\FieldInterface'); + + $field->expects($this->any()) + ->method('getDisplayedData') + ->will($this->returnValue($displayedData)); + $field->expects($this->any()) + ->method('getName') + ->will($this->returnValue($name)); + $field->expects($this->any()) + ->method('getId') + ->will($this->returnValue($id)); + + return $field; + } +} diff --git a/tests/Symfony/Tests/Components/Form/RepeatedFieldTest.php b/tests/Symfony/Tests/Components/Form/RepeatedFieldTest.php new file mode 100644 index 000000000000..9df4e6c6f93f --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/RepeatedFieldTest.php @@ -0,0 +1,53 @@ +field = new RepeatedField(new TestField('name')); + } + + public function testSetData() + { + $this->field->setData('foobar'); + + $this->assertEquals('foobar', $this->field['first']->getData()); + $this->assertEquals('foobar', $this->field['second']->getData()); + } + + public function testBindUnequal() + { + $input = array('first' => 'foo', 'second' => 'bar'); + + $this->field->bind($input); + + $this->assertEquals('foo', $this->field['first']->getDisplayedData()); + $this->assertEquals('bar', $this->field['second']->getDisplayedData()); + $this->assertFalse($this->field->isFirstEqualToSecond()); + $this->assertEquals($input, $this->field->getDisplayedData()); + $this->assertEquals(null, $this->field->getData()); + } + + public function testBindEqual() + { + $input = array('first' => 'foo', 'second' => 'foo'); + + $this->field->bind($input); + + $this->assertEquals('foo', $this->field['first']->getDisplayedData()); + $this->assertEquals('foo', $this->field['second']->getDisplayedData()); + $this->assertTrue($this->field->isFirstEqualToSecond()); + $this->assertEquals($input, $this->field->getDisplayedData()); + $this->assertEquals('foo', $this->field->getData()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/TextFieldTest.php b/tests/Symfony/Tests/Components/Form/TextFieldTest.php new file mode 100644 index 000000000000..d14d04b60726 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/TextFieldTest.php @@ -0,0 +1,30 @@ +setData('asdf'); + + $html = ''; + + $this->assertEquals($html, $field->render(array('class' => 'foobar'))); + } + + public function testRenderWithMaxLength() + { + $field = new TextField('name', array('max_length' => 10)); + $field->setData('asdf'); + + $html = ''; + + $this->assertEquals($html, $field->render()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/TextareaFieldTest.php b/tests/Symfony/Tests/Components/Form/TextareaFieldTest.php new file mode 100644 index 000000000000..679c65c578ca --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/TextareaFieldTest.php @@ -0,0 +1,30 @@ +setData('asdf'); + + $html = ''; + + $this->assertEquals($html, $field->render(array('class' => 'foobar'))); + } + + public function testRenderEscapesValue() + { + $field = new TextareaField('name'); + $field->setData('<&&'); + + $html = ''; + + $this->assertEquals($html, $field->render()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/TimeFieldTest.php b/tests/Symfony/Tests/Components/Form/TimeFieldTest.php new file mode 100644 index 000000000000..d7b4da820759 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/TimeFieldTest.php @@ -0,0 +1,261 @@ + 'UTC', + 'user_timezone' => 'UTC', + 'type' => TimeField::DATETIME, + )); + + $input = array( + 'hour' => '3', + 'minute' => '4', + ); + + $field->bind($input); + + $dateTime = new \DateTime('1970-01-01 03:04:00 UTC'); + + $this->assertEquals($dateTime, $field->getData()); + $this->assertEquals($input, $field->getDisplayedData()); + } + + public function testBind_string() + { + $field = new TimeField('name', array( + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + 'type' => TimeField::STRING, + )); + + $input = array( + 'hour' => '3', + 'minute' => '4', + ); + + $field->bind($input); + + $this->assertEquals('03:04:00', $field->getData()); + $this->assertEquals($input, $field->getDisplayedData()); + } + + public function testBind_timestamp() + { + $field = new TimeField('name', array( + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + 'type' => TimeField::TIMESTAMP, + )); + + $input = array( + 'hour' => '3', + 'minute' => '4', + ); + + $field->bind($input); + + $dateTime = new \DateTime('1970-01-01 03:04:00 UTC'); + + $this->assertEquals($dateTime->format('U'), $field->getData()); + $this->assertEquals($input, $field->getDisplayedData()); + } + + public function testBind_raw() + { + $field = new TimeField('name', array( + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + 'type' => TimeField::RAW, + )); + + $input = array( + 'hour' => '3', + 'minute' => '4', + ); + + $data = array( + 'hour' => '3', + 'minute' => '4', + 'second' => '0', + ); + + $field->bind($input); + + $this->assertEquals($data, $field->getData()); + $this->assertEquals($input, $field->getDisplayedData()); + } + + public function testSetData_withSeconds() + { + $field = new TimeField('name', array( + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + 'type' => TimeField::DATETIME, + 'with_seconds' => true, + )); + + $field->setData(new \DateTime('03:04:05 UTC')); + + $this->assertEquals(array('hour' => 3, 'minute' => 4, 'second' => 5), $field->getDisplayedData()); + } + + public function testSetData_differentTimezones() + { + $field = new TimeField('name', array( + 'data_timezone' => 'America/New_York', + 'user_timezone' => 'Pacific/Tahiti', + // don't do this test with DateTime, because it leads to wrong results! + 'type' => TimeField::STRING, + 'with_seconds' => true, + )); + + $dateTime = new \DateTime('03:04:05 America/New_York'); + + $field->setData($dateTime->format('H:i:s')); + + $dateTime = clone $dateTime; + $dateTime->setTimezone(new \DateTimeZone('Pacific/Tahiti')); + + $displayedData = array( + 'hour' => (int)$dateTime->format('H'), + 'minute' => (int)$dateTime->format('i'), + 'second' => (int)$dateTime->format('s') + ); + + $this->assertEquals($displayedData, $field->getDisplayedData()); + } + + public function testRenderAsInputs() + { + $field = new TimeField('name', array( + 'widget' => TimeField::INPUT, + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + )); + + $field->setData(new \DateTime('04:05 UTC')); + + $html = << +: + +EOF; + + $this->assertEquals(str_replace("\n", '', $html), $field->render(array('class' => 'foobar'))); + } + + public function testRenderAsInputs_withSeconds() + { + $field = new TimeField('name', array( + 'widget' => TimeField::INPUT, + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + 'with_seconds' => true, + )); + + $field->setData(new \DateTime('04:05:06 UTC')); + + $html = << +: + +: + +EOF; + + $this->assertEquals(str_replace("\n", '', $html), $field->render(array('class' => 'foobar'))); + } + + public function testRenderAsChoices() + { + $field = new TimeField('name', array( + 'hours' => array(3, 4), + 'minutes' => array(5, 6), + 'widget' => TimeField::CHOICE, + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + )); + + $field->setData(new \DateTime('04:05 UTC')); + + $html = << + + +: +EOF; + + $this->assertEquals($html, $field->render(array('class' => 'foobar'))); + } + + public function testRenderAsChoices_withSeconds() + { + $field = new TimeField('name', array( + 'hours' => array(3, 4), + 'minutes' => array(5, 6), + 'seconds' => array(7, 8), + 'widget' => TimeField::CHOICE, + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + 'with_seconds' => true, + )); + + $field->setData(new \DateTime('04:05:07 UTC')); + + $html = << + + +:: +EOF; + + $this->assertEquals($html, $field->render(array('class' => 'foobar'))); + } + + public function testRenderAsChoices_nonRequired() + { + $field = new TimeField('name', array( + 'hours' => array(3, 4), + 'minutes' => array(5, 6), + 'widget' => TimeField::CHOICE, + 'data_timezone' => 'UTC', + 'user_timezone' => 'UTC', + )); + + $field->setRequired(false); + $field->setData(new \DateTime('04:05 UTC')); + + $html = << + + + +: +EOF; + + $this->assertEquals($html, $field->render(array('class' => 'foobar'))); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/ToggleFieldTest.php b/tests/Symfony/Tests/Components/Form/ToggleFieldTest.php new file mode 100644 index 000000000000..c366b0ea7900 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/ToggleFieldTest.php @@ -0,0 +1,68 @@ +setData(true); + + $html = ''; + + $this->assertEquals($html, $field->render(array( + 'class' => 'foobar', + ))); + } + + public function testRender_deselected() + { + $field = new TestToggleField('name'); + $field->setData(false); + + $html = ''; + + $this->assertEquals($html, $field->render()); + } + + public function testRender_withValue() + { + $field = new TestToggleField('name', array('value' => 'foobar')); + + $html = ''; + + $this->assertEquals($html, $field->render()); + } + + public function testRender_withLabel() + { + $field = new TestToggleField('name', array('label' => 'foobar')); + + $html = ' '; + + $this->assertEquals($html, $field->render()); + } + + public function testRender_withTranslatedLabel() + { + $translator = $this->getMock('Symfony\Components\I18N\TranslatorInterface'); + $translator->expects($this->any()) + ->method('translate') + ->will($this->returnCallback(function($text) { + return 'translated['.$text.']'; + })); + + $field = new TestToggleField('name', array('label' => 'foobar', 'translate_label' => true)); + $field->setTranslator($translator); + + $html = ' '; + + $this->assertEquals($html, $field->render()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Form/ValueTransformer/BooleanToStringTransformerTest.php b/tests/Symfony/Tests/Components/Form/ValueTransformer/BooleanToStringTransformerTest.php new file mode 100644 index 000000000000..1a6319366022 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/ValueTransformer/BooleanToStringTransformerTest.php @@ -0,0 +1,45 @@ +transformer = new BooleanToStringTransformer(); + } + + public function testTransform() + { + $this->assertEquals('1', $this->transformer->transform(true)); + $this->assertEquals('', $this->transformer->transform(false)); + } + + public function testTransformExpectsBoolean() + { + $this->setExpectedException('\InvalidArgumentException'); + + $this->transformer->transform('1'); + } + + public function testReverseTransformExpectsString() + { + $this->setExpectedException('\InvalidArgumentException'); + + $this->transformer->reverseTransform(1); + } + + public function testReverseTransform() + { + $this->assertEquals(true, $this->transformer->reverseTransform('1')); + $this->assertEquals(true, $this->transformer->reverseTransform('0')); + $this->assertEquals(false, $this->transformer->reverseTransform('')); + } +} diff --git a/tests/Symfony/Tests/Components/Form/ValueTransformer/DateTimeToArrayTransformerTest.php b/tests/Symfony/Tests/Components/Form/ValueTransformer/DateTimeToArrayTransformerTest.php new file mode 100644 index 000000000000..96276cda3296 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/ValueTransformer/DateTimeToArrayTransformerTest.php @@ -0,0 +1,181 @@ + 'UTC', + 'output_timezone' => 'UTC', + )); + + $input = new \DateTime('2010-02-03 04:05:06 UTC'); + + $output = array( + 'year' => 2010, + 'month' => 2, + 'day' => 3, + 'hour' => 4, + 'minute' => 5, + 'second' => 6, + ); + + $this->assertSame($output, $transformer->transform($input)); + } + + public function testTransform_withFields() + { + $transformer = new DateTimeToArrayTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'fields' => array('year', 'month', 'minute', 'second'), + )); + + $input = new \DateTime('2010-02-03 04:05:06 UTC'); + + $output = array( + 'year' => 2010, + 'month' => 2, + 'minute' => 5, + 'second' => 6, + ); + + $this->assertSame($output, $transformer->transform($input)); + } + + public function testTransform_withPadding() + { + $transformer = new DateTimeToArrayTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'pad' => true, + )); + + $input = new \DateTime('2010-02-03 04:05:06 UTC'); + + $output = array( + 'year' => '2010', + 'month' => '02', + 'day' => '03', + 'hour' => '04', + 'minute' => '05', + 'second' => '06', + ); + + $this->assertSame($output, $transformer->transform($input)); + } + + public function testTransform_differentTimezones() + { + $transformer = new DateTimeToArrayTransformer(array( + 'input_timezone' => 'America/New_York', + 'output_timezone' => 'Asia/Hong_Kong', + )); + + $input = new \DateTime('2010-02-03 04:05:06 America/New_York'); + + $dateTime = new \DateTime('2010-02-03 04:05:06 America/New_York'); + $dateTime->setTimezone(new \DateTimeZone('Asia/Hong_Kong')); + $output = array( + 'year' => (int)$dateTime->format('Y'), + 'month' => (int)$dateTime->format('m'), + 'day' => (int)$dateTime->format('d'), + 'hour' => (int)$dateTime->format('H'), + 'minute' => (int)$dateTime->format('i'), + 'second' => (int)$dateTime->format('s'), + ); + + $this->assertSame($output, $transformer->transform($input)); + } + + public function testTransformRequiresDateTime() + { + $transformer = new DateTimeToArrayTransformer(); + + $this->setExpectedException('\InvalidArgumentException'); + + $transformer->reverseTransform('12345'); + } + + public function testReverseTransform() + { + $transformer = new DateTimeToArrayTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + )); + + $input = array( + 'year' => 2010, + 'month' => 2, + 'day' => 3, + 'hour' => 4, + 'minute' => 5, + 'second' => 6, + ); + + $output = new \DateTime('2010-02-03 04:05:06 UTC'); + + $this->assertDateTimeEquals($output, $transformer->reverseTransform($input)); + } + + public function testReverseTransform_differentTimezones() + { + $transformer = new DateTimeToArrayTransformer(array( + 'input_timezone' => 'America/New_York', + 'output_timezone' => 'Asia/Hong_Kong', + )); + + $input = array( + 'year' => 2010, + 'month' => 2, + 'day' => 3, + 'hour' => 4, + 'minute' => 5, + 'second' => 6, + ); + + $output = new \DateTime('2010-02-03 04:05:06 Asia/Hong_Kong'); + $output->setTimezone(new \DateTimeZone('America/New_York')); + + $this->assertDateTimeEquals($output, $transformer->reverseTransform($input)); + } + + public function testReverseTransformToDifferentTimezone() + { + $transformer = new DateTimeToArrayTransformer(array( + 'input_timezone' => 'Asia/Hong_Kong', + 'output_timezone' => 'UTC', + )); + + $input = array( + 'year' => 2010, + 'month' => 2, + 'day' => 3, + 'hour' => 4, + 'minute' => 5, + 'second' => 6, + ); + + $output = new \DateTime('2010-02-03 04:05:06 UTC'); + $output->setTimezone(new \DateTimeZone('Asia/Hong_Kong')); + + $this->assertDateTimeEquals($output, $transformer->reverseTransform($input)); + } + + public function testReverseTransformRequiresArray() + { + $transformer = new DateTimeToArrayTransformer(); + + $this->setExpectedException('\InvalidArgumentException'); + + $transformer->reverseTransform('12345'); + } +} diff --git a/tests/Symfony/Tests/Components/Form/ValueTransformer/DateTimeToLocalizedStringTransformerTest.php b/tests/Symfony/Tests/Components/Form/ValueTransformer/DateTimeToLocalizedStringTransformerTest.php new file mode 100644 index 000000000000..8ec4a4ff7367 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/ValueTransformer/DateTimeToLocalizedStringTransformerTest.php @@ -0,0 +1,310 @@ +dateTime = new \DateTime('2010-02-03 04:05:06 UTC'); + $this->dateTimeWithoutSeconds = new \DateTime('2010-02-03 04:05:00 UTC'); + } + + public static function assertEquals($expected, $actual, $message = '', $delta = 0, $maxDepth = 10, $canonicalize = FALSE, $ignoreCase = FALSE) + { + if ($expected instanceof \DateTime && $actual instanceof \DateTime) + { + $expected = $expected->format('c'); + $actual = $actual->format('c'); + } + + parent::assertEquals($expected, $actual, $message, $delta, $maxDepth, $canonicalize, $ignoreCase); + } + + public function testTransformShortDate() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'date_format' => 'short', + )); + $transformer->setLocale('de_AT'); + $this->assertEquals('03.02.10 04:05', $transformer->transform($this->dateTime)); + } + + public function testTransformMediumDate() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'date_format' => 'medium', + )); + $transformer->setLocale('de_AT'); + $this->assertEquals('03.02.2010 04:05', $transformer->transform($this->dateTime)); + } + + public function testTransformLongDate() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'date_format' => 'long', + )); + $transformer->setLocale('de_AT'); + $this->assertEquals('03. Februar 2010 04:05', $transformer->transform($this->dateTime)); + } + + public function testTransformFullDate() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'date_format' => 'full', + )); + $transformer->setLocale('de_AT'); + $this->assertEquals('Mittwoch, 03. Februar 2010 04:05', $transformer->transform($this->dateTime)); + } + + public function testTransformShortTime() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'time_format' => 'short', + )); + $transformer->setLocale('de_AT'); + $this->assertEquals('03.02.2010 04:05', $transformer->transform($this->dateTime)); + } + + public function testTransformMediumTime() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'time_format' => 'medium', + )); + $transformer->setLocale('de_AT'); + $this->assertEquals('03.02.2010 04:05:06', $transformer->transform($this->dateTime)); + } + + public function testTransformLongTime() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'time_format' => 'long', + )); + $transformer->setLocale('de_AT'); + $this->assertEquals('03.02.2010 04:05:06 GMT+00:00', $transformer->transform($this->dateTime)); + } + + public function testTransformFullTime() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'time_format' => 'full', + )); + $transformer->setLocale('de_AT'); + $this->assertEquals('03.02.2010 04:05:06 GMT+00:00', $transformer->transform($this->dateTime)); + } + + public function testTransformToDifferentLocale() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + )); + $transformer->setLocale('en_US'); + $this->assertEquals('Feb 3, 2010 4:05 AM', $transformer->transform($this->dateTime)); + } + + public function testTransform_differentTimezones() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'America/New_York', + 'output_timezone' => 'Asia/Hong_Kong', + )); + $transformer->setLocale('de_AT'); + + $input = new \DateTime('2010-02-03 04:05:06 America/New_York'); + + $dateTime = clone $input; + $dateTime->setTimezone(new \DateTimeZone('Asia/Hong_Kong')); + + $this->assertEquals($dateTime->format('d.m.Y H:i'), $transformer->transform($input)); + } + + public function testTransformRequiresValidDateTime() + { + $transformer = new DateTimeToLocalizedStringTransformer(); + + $this->setExpectedException('\InvalidArgumentException'); + + $transformer->transform('2010-01-01'); + } + + public function testTransformWrapsIntlErrors() + { + $transformer = new DateTimeToLocalizedStringTransformer(); + + // HOW TO REPRODUCE? + + //$this->setExpectedException('Symfony\Components\Form\ValueTransformer\Transdate_formationFailedException'); + + //$transformer->transform(1.5); + } + + public function testReverseTransformShortDate() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'date_format' => 'short', + )); + $transformer->setLocale('de_AT'); + $this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('03.02.10 04:05')); + } + + public function testReverseTransformMediumDate() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'date_format' => 'medium', + )); + $transformer->setLocale('de_AT'); + $this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('03.02.2010 04:05')); + } + + public function testReverseTransformLongDate() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'date_format' => 'long', + )); + $transformer->setLocale('de_AT'); + $this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('03. Februar 2010 04:05')); + } + + public function testReverseTransformFullDate() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'date_format' => 'full', + )); + $transformer->setLocale('de_AT'); + $this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('Mittwoch, 03. Februar 2010 04:05')); + } + + public function testReverseTransformShortTime() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'time_format' => 'short', + )); + $transformer->setLocale('de_AT'); + $this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('03.02.2010 04:05')); + } + + public function testReverseTransformMediumTime() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'time_format' => 'medium', + )); + $transformer->setLocale('de_AT'); + $this->assertDateTimeEquals($this->dateTime, $transformer->reverseTransform('03.02.2010 04:05:06')); + } + + public function testReverseTransformLongTime() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'time_format' => 'long', + )); + $transformer->setLocale('de_AT'); + $this->assertDateTimeEquals($this->dateTime, $transformer->reverseTransform('03.02.2010 04:05:06 GMT+00:00')); + } + + public function testReverseTransformFullTime() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + 'time_format' => 'full', + )); + $transformer->setLocale('de_AT'); + $this->assertDateTimeEquals($this->dateTime, $transformer->reverseTransform('03.02.2010 04:05:06 GMT+00:00')); + } + + public function testReverseTransformFromDifferentLocale() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + )); + $transformer->setLocale('en_US'); + $this->assertDateTimeEquals($this->dateTimeWithoutSeconds, $transformer->reverseTransform('Feb 3, 2010 04:05 AM')); + } + + public function testReverseTransform_differentTimezones() + { + $transformer = new DateTimeToLocalizedStringTransformer(array( + 'input_timezone' => 'America/New_York', + 'output_timezone' => 'Asia/Hong_Kong', + )); + $transformer->setLocale('de_AT'); + + $dateTime = new \DateTime('2010-02-03 04:05:00 Asia/Hong_Kong'); + $dateTime->setTimezone(new \DateTimeZone('America/New_York')); + + $this->assertDateTimeEquals($dateTime, $transformer->reverseTransform('03.02.2010 04:05')); + } + + public function testReverseTransformRequiresString() + { + $transformer = new DateTimeToLocalizedStringTransformer(); + + $this->setExpectedException('\InvalidArgumentException'); + + $transformer->reverseTransform(12345); + } + + public function testReverseTransformWrapsIntlErrors() + { + $transformer = new DateTimeToLocalizedStringTransformer(); + + $this->setExpectedException('Symfony\Components\Form\ValueTransformer\TransformationFailedException'); + + $transformer->reverseTransform('12345'); + } + + public function testValidateDateFormatOption() + { + $this->setExpectedException('\InvalidArgumentException'); + + new DateTimeToLocalizedStringTransformer(array('date_format' => 'foobar')); + } + + public function testValidateTimeFormatOption() + { + $this->setExpectedException('\InvalidArgumentException'); + + new DateTimeToLocalizedStringTransformer(array('time_format' => 'foobar')); + } +} diff --git a/tests/Symfony/Tests/Components/Form/ValueTransformer/NumberToLocalizedStringTransformerTest.php b/tests/Symfony/Tests/Components/Form/ValueTransformer/NumberToLocalizedStringTransformerTest.php new file mode 100644 index 000000000000..34ae20e9b9ba --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/ValueTransformer/NumberToLocalizedStringTransformerTest.php @@ -0,0 +1,86 @@ +setLocale('de_AT'); + + $this->assertEquals('1', $transformer->transform(1)); + $this->assertEquals('1,5', $transformer->transform(1.5)); + $this->assertEquals('1234,5', $transformer->transform(1234.5)); + $this->assertEquals('12345,912', $transformer->transform(12345.9123)); + } + + public function testTransformWithGrouping() + { + $transformer = new NumberToLocalizedStringTransformer(array( + 'grouping' => true, + )); + $transformer->setLocale('de_AT'); + + $this->assertEquals('1.234,5', $transformer->transform(1234.5)); + $this->assertEquals('12.345,912', $transformer->transform(12345.9123)); + } + + public function testTransformWithPrecision() + { + $transformer = new NumberToLocalizedStringTransformer(array( + 'precision' => 2, + )); + $transformer->setLocale('de_AT'); + + $this->assertEquals('1234,50', $transformer->transform(1234.5)); + $this->assertEquals('678,92', $transformer->transform(678.916)); + } + + public function testReverseTransform() + { + $transformer = new NumberToLocalizedStringTransformer(); + $transformer->setLocale('de_AT'); + + $this->assertEquals(1, $transformer->reverseTransform('1')); + $this->assertEquals(1.5, $transformer->reverseTransform('1,5')); + $this->assertEquals(1234.5, $transformer->reverseTransform('1234,5')); + $this->assertEquals(12345.912, $transformer->reverseTransform('12345,912')); + } + + public function testReverseTransformWithGrouping() + { + $transformer = new NumberToLocalizedStringTransformer(array( + 'grouping' => true, + )); + $transformer->setLocale('de_AT'); + + $this->assertEquals(1234.5, $transformer->reverseTransform('1.234,5')); + $this->assertEquals(12345.912, $transformer->reverseTransform('12.345,912')); + $this->assertEquals(1234.5, $transformer->reverseTransform('1234,5')); + $this->assertEquals(12345.912, $transformer->reverseTransform('12345,912')); + } + + public function testTransformExpectsNumeric() + { + $transformer = new NumberToLocalizedStringTransformer(); + + $this->setExpectedException('\InvalidArgumentException'); + + $transformer->transform('foo'); + } + + public function testReverseTransformExpectsString() + { + $transformer = new NumberToLocalizedStringTransformer(); + + $this->setExpectedException('\InvalidArgumentException'); + + $transformer->reverseTransform(1); + } +} diff --git a/tests/Symfony/Tests/Components/Form/ValueTransformer/PercentToLocalizedStringTransformerTest.php b/tests/Symfony/Tests/Components/Form/ValueTransformer/PercentToLocalizedStringTransformerTest.php new file mode 100644 index 000000000000..ec1c8c2dd9a2 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/ValueTransformer/PercentToLocalizedStringTransformerTest.php @@ -0,0 +1,97 @@ +setLocale('de_AT'); + + $this->assertEquals('10', $transformer->transform(0.1)); + $this->assertEquals('15', $transformer->transform(0.15)); + $this->assertEquals('12', $transformer->transform(0.1234)); + $this->assertEquals('200', $transformer->transform(2)); + } + + public function testTransformWithInteger() + { + $transformer = new PercentToLocalizedStringTransformer(array( + 'type' => 'integer', + )); + $transformer->setLocale('de_AT'); + + $this->assertEquals('0', $transformer->transform(0.1)); + $this->assertEquals('1', $transformer->transform(1)); + $this->assertEquals('15', $transformer->transform(15)); + $this->assertEquals('16', $transformer->transform(15.9)); + } + + public function testTransformWithPrecision() + { + $transformer = new PercentToLocalizedStringTransformer(array( + 'precision' => 2, + )); + $transformer->setLocale('de_AT'); + + $this->assertEquals('12,34', $transformer->transform(0.1234)); + } + + public function testReverseTransform() + { + $transformer = new PercentToLocalizedStringTransformer(); + $transformer->setLocale('de_AT'); + + $this->assertEquals(0.1, $transformer->reverseTransform('10')); + $this->assertEquals(0.15, $transformer->reverseTransform('15')); + $this->assertEquals(0.12, $transformer->reverseTransform('12')); + $this->assertEquals(2, $transformer->reverseTransform('200')); + } + + public function testReverseTransformWithInteger() + { + $transformer = new PercentToLocalizedStringTransformer(array( + 'type' => 'integer', + )); + $transformer->setLocale('de_AT'); + + $this->assertEquals(10, $transformer->reverseTransform('10')); + $this->assertEquals(15, $transformer->reverseTransform('15')); + $this->assertEquals(12, $transformer->reverseTransform('12')); + $this->assertEquals(200, $transformer->reverseTransform('200')); + } + + public function testReverseTransformWithPrecision() + { + $transformer = new PercentToLocalizedStringTransformer(array( + 'precision' => 2, + )); + $transformer->setLocale('de_AT'); + + $this->assertEquals(0.1234, $transformer->reverseTransform('12,34')); + } + + public function testTransformExpectsNumeric() + { + $transformer = new PercentToLocalizedStringTransformer(); + + $this->setExpectedException('\InvalidArgumentException'); + + $transformer->transform('foo'); + } + + public function testReverseTransformExpectsString() + { + $transformer = new PercentToLocalizedStringTransformer(); + + $this->setExpectedException('\InvalidArgumentException'); + + $transformer->reverseTransform(1); + } +} diff --git a/tests/Symfony/Tests/Components/Form/ValueTransformer/StringToDateTimeTransformerTest.php b/tests/Symfony/Tests/Components/Form/ValueTransformer/StringToDateTimeTransformerTest.php new file mode 100644 index 000000000000..3a5b7d896194 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/ValueTransformer/StringToDateTimeTransformerTest.php @@ -0,0 +1,84 @@ + 'UTC', + 'output_timezone' => 'UTC', + )); + + $output = new \DateTime('2010-02-03 04:05:06 UTC'); + $input = $output->format('Y-m-d H:i:s'); + + $this->assertDateTimeEquals($output, $transformer->transform($input)); + } + + public function testTransform_differentTimezones() + { + $transformer = new StringToDateTimeTransformer(array( + 'input_timezone' => 'America/New_York', + 'output_timezone' => 'Asia/Hong_Kong', + )); + + $output = new \DateTime('2010-02-03 04:05:06 America/New_York'); + $input = $output->format('Y-m-d H:i:s'); + $output->setTimeZone(new \DateTimeZone('Asia/Hong_Kong')); + + $this->assertDateTimeEquals($output, $transformer->transform($input)); + } + + public function testTransformExpectsValidString() + { + $transformer = new StringToDateTimeTransformer(); + + $this->setExpectedException('\InvalidArgumentException'); + $transformer->transform('2010-2010-2010'); + } + + public function testReverseTransform() + { + $transformer = new StringToDateTimeTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + )); + + $input = new \DateTime('2010-02-03 04:05:06 UTC'); + $output = clone $input; + $output->setTimezone(new \DateTimeZone('UTC')); + $output = $output->format('Y-m-d H:i:s'); + + $this->assertEquals($output, $transformer->reverseTransform($input)); + } + + public function testReverseTransform_differentTimezones() + { + $transformer = new StringToDateTimeTransformer(array( + 'input_timezone' => 'Asia/Hong_Kong', + 'output_timezone' => 'America/New_York', + )); + + $input = new \DateTime('2010-02-03 04:05:06 Asia/Hong_Kong'); + $output = $input->format('Y-m-d H:i:s'); + $input->setTimezone(new \DateTimeZone('America/New_York')); + + $this->assertEquals($output, $transformer->reverseTransform($input)); + } + + public function testReverseTransformExpectsDateTime() + { + $transformer = new StringToDateTimeTransformer(); + + $this->setExpectedException('\InvalidArgumentException'); + $transformer->reverseTransform('1234'); + } +} diff --git a/tests/Symfony/Tests/Components/Form/ValueTransformer/TimestampToDateTimeTransformerTest.php b/tests/Symfony/Tests/Components/Form/ValueTransformer/TimestampToDateTimeTransformerTest.php new file mode 100644 index 000000000000..3d29cf1b6097 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/ValueTransformer/TimestampToDateTimeTransformerTest.php @@ -0,0 +1,98 @@ + 'UTC', + 'output_timezone' => 'UTC', + )); + + $output = new \DateTime('2010-02-03 04:05:06 UTC'); + $input = $output->format('U'); + + $this->assertDateTimeEquals($output, $transformer->transform($input)); + } + + public function testTransform_differentTimezones() + { + $transformer = new TimestampToDateTimeTransformer(array( + 'input_timezone' => 'Asia/Hong_Kong', + 'output_timezone' => 'America/New_York', + )); + + $output = new \DateTime('2010-02-03 04:05:06 Asia/Hong_Kong'); + $input = $output->format('U'); + $output->setTimezone(new \DateTimeZone('America/New_York')); + + $this->assertDateTimeEquals($output, $transformer->transform($input)); + } + + public function testReverseTransformExpectsValidTimestamp() + { + $transformer = new TimestampToDateTimeTransformer(); + + $this->setExpectedException('\InvalidArgumentException'); + $transformer->transform('2010-2010-2010'); + } + + public function testReverseTransform() + { + $transformer = new TimestampToDateTimeTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'UTC', + )); + + $input = new \DateTime('2010-02-03 04:05:06 UTC'); + $output = $input->format('U'); + + $this->assertEquals($output, $transformer->reverseTransform($input)); + } + + public function testReverseTransform_differentTimezones() + { + $transformer = new TimestampToDateTimeTransformer(array( + 'input_timezone' => 'Asia/Hong_Kong', + 'output_timezone' => 'America/New_York', + )); + + $input = new \DateTime('2010-02-03 04:05:06 Asia/Hong_Kong'); + $output = $input->format('U'); + $input->setTimezone(new \DateTimeZone('America/New_York')); + + $this->assertEquals($output, $transformer->reverseTransform($input)); + } + + public function testReverseTransformFromDifferentTimezone() + { + $transformer = new TimestampToDateTimeTransformer(array( + 'input_timezone' => 'UTC', + 'output_timezone' => 'Asia/Hong_Kong', + )); + + $input = new \DateTime('2010-02-03 04:05:06 Asia/Hong_Kong'); + + $dateTime = clone $input; + $dateTime->setTimezone(new \DateTimeZone('UTC')); + $output = $dateTime->format('U'); + + $this->assertEquals($output, $transformer->reverseTransform($input)); + } + + public function testReverseTransformExpectsDateTime() + { + $transformer = new TimestampToDateTimeTransformer(); + + $this->setExpectedException('\InvalidArgumentException'); + $transformer->reverseTransform('1234'); + } +} diff --git a/tests/Symfony/Tests/Components/Form/ValueTransformer/ValueTransformerChainTest.php b/tests/Symfony/Tests/Components/Form/ValueTransformer/ValueTransformerChainTest.php new file mode 100644 index 000000000000..cdafe334da93 --- /dev/null +++ b/tests/Symfony/Tests/Components/Form/ValueTransformer/ValueTransformerChainTest.php @@ -0,0 +1,63 @@ +getMock('Symfony\Components\Form\ValueTransformer\ValueTransformerInterface'); + $transformer1->expects($this->once()) + ->method('transform') + ->with($this->identicalTo('foo')) + ->will($this->returnValue('bar')); + $transformer2 = $this->getMock('Symfony\Components\Form\ValueTransformer\ValueTransformerInterface'); + $transformer2->expects($this->once()) + ->method('transform') + ->with($this->identicalTo('bar')) + ->will($this->returnValue('baz')); + + $chain = new ValueTransformerChain(array($transformer1, $transformer2)); + + $this->assertEquals('baz', $chain->transform('foo')); + } + + public function testReverseTransform() + { + $transformer2 = $this->getMock('Symfony\Components\Form\ValueTransformer\ValueTransformerInterface'); + $transformer2->expects($this->once()) + ->method('reverseTransform') + ->with($this->identicalTo('foo')) + ->will($this->returnValue('bar')); + $transformer1 = $this->getMock('Symfony\Components\Form\ValueTransformer\ValueTransformerInterface'); + $transformer1->expects($this->once()) + ->method('reverseTransform') + ->with($this->identicalTo('bar')) + ->will($this->returnValue('baz')); + + $chain = new ValueTransformerChain(array($transformer1, $transformer2)); + + $this->assertEquals('baz', $chain->reverseTransform('foo')); + } + + public function testSetLocale() + { + $transformer1 = $this->getMock('Symfony\Components\Form\ValueTransformer\ValueTransformerInterface'); + $transformer1->expects($this->once()) + ->method('setLocale') + ->with($this->identicalTo('de_DE')); + $transformer2 = $this->getMock('Symfony\Components\Form\ValueTransformer\ValueTransformerInterface'); + $transformer2->expects($this->once()) + ->method('setLocale') + ->with($this->identicalTo('de_DE')); + + $chain = new ValueTransformerChain(array($transformer1, $transformer2)); + + $chain->setLocale('de_DE'); + } +} diff --git a/tests/Symfony/Tests/Components/Validator/ConstraintTest.php b/tests/Symfony/Tests/Components/Validator/ConstraintTest.php new file mode 100644 index 000000000000..385a154ec709 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/ConstraintTest.php @@ -0,0 +1,86 @@ + 'foo', + 'property2' => 'bar', + )); + + $this->assertEquals('foo', $constraint->property1); + $this->assertEquals('bar', $constraint->property2); + } + + public function testSetNotExistingPropertyThrowsException() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\InvalidOptionsException'); + + new ConstraintA(array( + 'foo' => 'bar', + )); + } + + public function testMagicPropertiesAreNotAllowed() + { + $constraint = new ConstraintA(); + + $this->setExpectedException('Symfony\Components\Validator\Exception\InvalidOptionsException'); + + $constraint->foo = 'bar'; + } + + public function testSetDefaultProperty() + { + $constraint = new ConstraintA('foo'); + + $this->assertEquals('foo', $constraint->property2); + } + + public function testSetDefaultPropertyDoctrineStyle() + { + $constraint = new ConstraintA(array('value' => 'foo')); + + $this->assertEquals('foo', $constraint->property2); + } + + public function testSetUndefinedDefaultProperty() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\ConstraintDefinitionException'); + + new ConstraintB('foo'); + } + + public function testRequiredOptionsMustBeDefined() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\MissingOptionsException'); + + new ConstraintC(); + } + + public function testGroupsAreConvertedToArray() + { + $constraint = new ConstraintA(array('groups' => 'Foo')); + + $this->assertEquals(array('Foo'), $constraint->groups); + } + + public function testAddDefaultGroupAddsGroup() + { + $constraint = new ConstraintA(array('groups' => 'Default')); + $constraint->addImplicitGroupName('Foo'); + $this->assertEquals(array('Default', 'Foo'), $constraint->groups); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/AllValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/AllValidatorTest.php new file mode 100644 index 000000000000..a39756553bc8 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/AllValidatorTest.php @@ -0,0 +1,94 @@ +walker = $this->getMock('Symfony\Components\Validator\GraphWalker', array(), array(), '', false); + $metadataFactory = $this->getMock('Symfony\Components\Validator\Mapping\ClassMetadataFactoryInterface'); + $messageInterpolator = $this->getMock('Symfony\Components\Validator\MessageInterpolator\MessageInterpolatorInterface'); + + $this->context = new ValidationContext('Root', $this->walker, $metadataFactory, $messageInterpolator); + + $this->validator = new AllValidator(); + $this->validator->initialize($this->context); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new All(new Min(4)))); + } + + public function testThrowsExceptionIfNotTraversable() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid('foobar', new All(new Min(4))); + } + + /** + * @dataProvider getValidArguments + */ + public function testWalkSingleConstraint($array) + { + $this->context->setGroup('MyGroup'); + $this->context->setPropertyPath('foo'); + + $constraint = new Min(4); + + foreach ($array as $key => $value) + { + $this->walker->expects($this->once()) + ->method('walkConstraint') + ->with($this->equalTo($constraint), $this->equalTo($value), $this->equalTo('MyGroup'), $this->equalTo('foo['.$key.']')); + } + + $this->assertTrue($this->validator->isValid($array, new All($constraint))); + } + + /** + * @dataProvider getValidArguments + */ + public function testWalkMultipleConstraints($array) + { + $this->context->setGroup('MyGroup'); + $this->context->setPropertyPath('foo'); + + $constraint = new Min(4); + // can only test for twice the same constraint because PHPUnits mocking + // can't test method calls with different arguments + $constraints = array($constraint, $constraint); + + foreach ($array as $key => $value) + { + $this->walker->expects($this->exactly(2)) + ->method('walkConstraint') + ->with($this->equalTo($constraint), $this->equalTo($value), $this->equalTo('MyGroup'), $this->equalTo('foo['.$key.']')); + } + + $this->assertTrue($this->validator->isValid($array, new All($constraints))); + } + + public function getValidArguments() + { + return array( + // can only test for one entry, because PHPUnits mocking does not allow + // to expect multiple method calls with different arguments + array(array(1)), + array(new \ArrayObject(array(1))), + ); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/AssertFalseValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/AssertFalseValidatorTest.php new file mode 100644 index 000000000000..c0250e857c70 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/AssertFalseValidatorTest.php @@ -0,0 +1,39 @@ +validator = new AssertFalseValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new AssertFalse())); + } + + public function testFalseIsValid() + { + $this->assertTrue($this->validator->isValid(false, new AssertFalse())); + } + + public function testTrueIsInvalid() + { + $constraint = new AssertFalse(array( + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid(true, $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/AssertTrueValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/AssertTrueValidatorTest.php new file mode 100644 index 000000000000..06261e72ed67 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/AssertTrueValidatorTest.php @@ -0,0 +1,39 @@ +validator = new AssertTrueValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new AssertTrue())); + } + + public function testTrueIsValid() + { + $this->assertTrue($this->validator->isValid(true, new AssertTrue())); + } + + public function testFalseIsInvalid() + { + $constraint = new AssertTrue(array( + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid(false, $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/AssertTypeValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/AssertTypeValidatorTest.php new file mode 100644 index 000000000000..ad6c71d3e2e7 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/AssertTypeValidatorTest.php @@ -0,0 +1,128 @@ +validator = new AssertTypeValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new AssertType(array('type' => 'integer')))); + } + + /** + * @dataProvider getValidValues + */ + public function testValidValues($value, $type) + { + $constraint = new AssertType(array('type' => $type)); + + $this->assertTrue($this->validator->isValid($value, $constraint)); + } + + public function getValidValues() + { + $object = new \stdClass(); + $file = $this->createFile(); + + return array( + array(true, 'boolean'), + array(false, 'boolean'), + array(true, 'bool'), + array(false, 'bool'), + array(0, 'numeric'), + array('0', 'numeric'), + array(1.5, 'numeric'), + array('1.5', 'numeric'), + array(0, 'integer'), + array(1.5, 'float'), + array('12345', 'string'), + array(array(), 'array'), + array($object, 'object'), + array($object, 'stdClass'), + array($file, 'resource'), + ); + } + + /** + * @dataProvider getInvalidValues + */ + public function testInvalidValues($value, $type) + { + $constraint = new AssertType(array('type' => $type)); + + $this->assertFalse($this->validator->isValid($value, $constraint)); + } + + public function getInvalidValues() + { + $object = new \stdClass(); + $file = $this->createFile(); + + return array( + array('foobar', 'numeric'), + array('foobar', 'boolean'), + array('0', 'integer'), + array('1.5', 'float'), + array(12345, 'string'), + array($object, 'boolean'), + array($object, 'numeric'), + array($object, 'integer'), + array($object, 'float'), + array($object, 'string'), + array($object, 'resource'), + array($file, 'boolean'), + array($file, 'numeric'), + array($file, 'integer'), + array($file, 'float'), + array($file, 'string'), + array($file, 'object'), + ); + } + + public function testMessageIsSet() + { + $constraint = new AssertType(array( + 'type' => 'numeric', + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid('foobar', $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => 'foobar', + 'type' => 'numeric', + )); + } + + protected function createFile() + { + if (!self::$file) + { + self::$file = fopen(__FILE__, 'r'); + } + + return self::$file; + } + + public static function tearDownAfterClass() + { + if (self::$file) + { + fclose(self::$file); + } + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/BlankValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/BlankValidatorTest.php new file mode 100644 index 000000000000..05279d21098f --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/BlankValidatorTest.php @@ -0,0 +1,59 @@ +validator = new BlankValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new Blank())); + } + + public function testBlankIsValid() + { + $this->assertTrue($this->validator->isValid('', new Blank())); + } + + /** + * @dataProvider getInvalidValues + */ + public function testInvalidValues($date) + { + $this->assertFalse($this->validator->isValid($date, new Blank())); + } + + public function getInvalidValues() + { + return array( + array('foobar'), + array(0), + array(false), + array(1234), + ); + } + + public function testMessageIsSet() + { + $constraint = new Blank(array( + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid('foobar', $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => 'foobar', + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/ChoiceValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/ChoiceValidatorTest.php new file mode 100644 index 000000000000..c7fa670451a1 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/ChoiceValidatorTest.php @@ -0,0 +1,174 @@ +getMock('Symfony\Components\Validator\MessageInterpolator\MessageInterpolatorInterface'); + $walker = $this->getMock('Symfony\Components\Validator\GraphWalker', array(), array(), '', false); + $factory = $this->getMock('Symfony\Components\Validator\Mapping\ClassMetadataFactoryInterface'); + $context = new ValidationContext('root', $walker, $factory, $interpolator); + $context->setCurrentClass(__CLASS__); + $this->validator = new ChoiceValidator(); + $this->validator->initialize($context); + } + + public function testExpectArrayIfMultipleIsTrue() + { + $constraint = new Choice(array( + 'choices' => array('foo', 'bar'), + 'multiple' => true, + )); + + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid('asdf', $constraint); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new Choice(array('choices' => array('foo', 'bar'))))); + } + + public function testChoicesOrCallbackExpected() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\ConstraintDefinitionException'); + + $this->validator->isValid('foobar', new Choice()); + } + + public function testValidCallbackExpected() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\ConstraintDefinitionException'); + + $this->validator->isValid('foobar', new Choice(array('callback' => 'abcd'))); + } + + public function testValidChoiceArray() + { + $constraint = new Choice(array('choices' => array('foo', 'bar'))); + + $this->assertTrue($this->validator->isValid('bar', $constraint)); + } + + public function testValidChoiceCallbackFunction() + { + $constraint = new Choice(array('callback' => __NAMESPACE__.'\choice_callback')); + + $this->assertTrue($this->validator->isValid('bar', $constraint)); + } + + public function testValidChoiceCallbackClosure() + { + $constraint = new Choice(array('callback' => function() { + return array('foo', 'bar'); + })); + + $this->assertTrue($this->validator->isValid('bar', $constraint)); + } + + public function testValidChoiceCallbackStaticMethod() + { + $constraint = new Choice(array('callback' => array(__CLASS__, 'staticCallback'))); + + $this->assertTrue($this->validator->isValid('bar', $constraint)); + } + + public function testValidChoiceCallbackContextMethod() + { + $constraint = new Choice(array('callback' => 'staticCallback')); + + $this->assertTrue($this->validator->isValid('bar', $constraint)); + } + + public function testMultipleChoices() + { + $constraint = new Choice(array( + 'choices' => array('foo', 'bar', 'baz'), + 'multiple' => true, + )); + + $this->assertTrue($this->validator->isValid(array('baz', 'bar'), $constraint)); + } + + public function testInvalidChoice() + { + $constraint = new Choice(array( + 'choices' => array('foo', 'bar'), + 'message' => 'myMessage', + )); + + $this->assertFalse($this->validator->isValid('baz', $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => 'baz', + )); + } + + public function testInvalidChoiceMultiple() + { + $constraint = new Choice(array( + 'choices' => array('foo', 'bar'), + 'message' => 'myMessage', + 'multiple' => true, + )); + + $this->assertFalse($this->validator->isValid(array('foo', 'baz'), $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => 'baz', + )); + } + + public function testTooFewChoices() + { + $constraint = new Choice(array( + 'choices' => array('foo', 'bar', 'moo', 'maa'), + 'multiple' => true, + 'min' => 2, + 'minMessage' => 'myMessage', + )); + + $this->assertFalse($this->validator->isValid(array('foo'), $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'limit' => 2, + )); + } + + public function testTooManyChoices() + { + $constraint = new Choice(array( + 'choices' => array('foo', 'bar', 'moo', 'maa'), + 'multiple' => true, + 'max' => 2, + 'maxMessage' => 'myMessage', + )); + + $this->assertFalse($this->validator->isValid(array('foo', 'bar', 'moo'), $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'limit' => 2, + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/CollectionValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/CollectionValidatorTest.php new file mode 100644 index 000000000000..33f2c0aff6d4 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/CollectionValidatorTest.php @@ -0,0 +1,184 @@ +walker = $this->getMock('Symfony\Components\Validator\GraphWalker', array(), array(), '', false); + $metadataFactory = $this->getMock('Symfony\Components\Validator\Mapping\ClassMetadataFactoryInterface'); + $messageInterpolator = $this->getMock('Symfony\Components\Validator\MessageInterpolator\MessageInterpolatorInterface'); + + $this->context = new ValidationContext('Root', $this->walker, $metadataFactory, $messageInterpolator); + + $this->validator = new CollectionValidator(); + $this->validator->initialize($this->context); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new Collection(array('fields' => array( + 'foo' => new Min(4), + ))))); + } + + public function testThrowsExceptionIfNotTraversable() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid('foobar', new Collection(array('fields' => array( + 'foo' => new Min(4), + )))); + } + + /** + * @dataProvider getValidArguments + */ + public function testWalkSingleConstraint($array) + { + $this->context->setGroup('MyGroup'); + $this->context->setPropertyPath('foo'); + + $constraint = new Min(4); + + foreach ($array as $key => $value) + { + $this->walker->expects($this->once()) + ->method('walkConstraint') + ->with($this->equalTo($constraint), $this->equalTo($value), $this->equalTo('MyGroup'), $this->equalTo('foo['.$key.']')); + } + + $this->assertTrue($this->validator->isValid($array, new Collection(array( + 'fields' => array( + 'foo' => $constraint, + ), + )))); + } + + /** + * @dataProvider getValidArguments + */ + public function testWalkMultipleConstraints($array) + { + $this->context->setGroup('MyGroup'); + $this->context->setPropertyPath('foo'); + + $constraint = new Min(4); + // can only test for twice the same constraint because PHPUnits mocking + // can't test method calls with different arguments + $constraints = array($constraint, $constraint); + + foreach ($array as $key => $value) + { + $this->walker->expects($this->exactly(2)) + ->method('walkConstraint') + ->with($this->equalTo($constraint), $this->equalTo($value), $this->equalTo('MyGroup'), $this->equalTo('foo['.$key.']')); + } + + $this->assertTrue($this->validator->isValid($array, new Collection(array( + 'fields' => array( + 'foo' => $constraints, + ) + )))); + } + + public function testExtraFieldsDisallowed() + { + $array = array( + 'foo' => 5, + 'bar' => 6, + ); + + $this->assertFalse($this->validator->isValid($array, new Collection(array( + 'fields' => array( + 'foo' => new Min(4), + ), + )))); + } + + // bug fix + public function testNullNotConsideredExtraField() + { + $array = array( + 'foo' => null, + ); + + $this->assertTrue($this->validator->isValid($array, new Collection(array( + 'fields' => array( + 'foo' => new Min(4), + ), + )))); + } + + public function testExtraFieldsAllowed() + { + $array = array( + 'foo' => 5, + 'bar' => 6, + ); + + $this->assertTrue($this->validator->isValid($array, new Collection(array( + 'fields' => array( + 'foo' => new Min(4), + ), + 'allowExtraFields' => true, + )))); + } + + public function testMissingFieldsDisallowed() + { + $this->assertFalse($this->validator->isValid(array(), new Collection(array( + 'fields' => array( + 'foo' => new Min(4), + ), + )))); + } + + public function testMissingFieldsAllowed() + { + $this->assertTrue($this->validator->isValid(array(), new Collection(array( + 'fields' => array( + 'foo' => new Min(4), + ), + 'allowMissingFields' => true, + )))); + } + + public function getValidArguments() + { + return array( + // can only test for one entry, because PHPUnits mocking does not allow + // to expect multiple method calls with different arguments + array(array('foo' => 3)), + array(new \ArrayObject(array('foo' => 3))), + ); + } + + public function testObjectShouldBeLeftUnchanged() + { + $value = new \ArrayObject(array( + 'foo' => 3 + )); + $this->validator->isValid($value, new Collection(array( + 'fields' => array( + 'foo' => new Min(2), + ) + ))); + + $this->assertEquals(array( + 'foo' => 3 + ), (array) $value); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/DateTimeValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/DateTimeValidatorTest.php new file mode 100644 index 000000000000..b242b3265c3d --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/DateTimeValidatorTest.php @@ -0,0 +1,84 @@ +validator = new DateTimeValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new DateTime())); + } + + public function testExpectsStringCompatibleType() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid(new \stdClass(), new DateTime()); + } + + /** + * @dataProvider getValidDateTimes + */ + public function testValidDateTimes($date) + { + $this->assertTrue($this->validator->isValid($date, new DateTime())); + } + + public function getValidDateTimes() + { + return array( + array('2010-01-01 01:02:03'), + array('1955-12-12 00:00:00'), + array('2030-05-31 23:59:59'), + ); + } + + /** + * @dataProvider getInvalidDateTimes + */ + public function testInvalidDateTimes($date) + { + $this->assertFalse($this->validator->isValid($date, new DateTime())); + } + + public function getInvalidDateTimes() + { + return array( + array('foobar'), + array('2010-01-01'), + array('00:00:00'), + array('2010-01-01 00:00'), + array('2010-13-01 00:00:00'), + array('2010-04-32 00:00:00'), + array('2010-02-29 00:00:00'), + array('2010-01-01 24:00:00'), + array('2010-01-01 00:60:00'), + array('2010-01-01 00:00:60'), + ); + } + + public function testMessageIsSet() + { + $constraint = new DateTime(array( + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid('foobar', $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => 'foobar', + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/DateValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/DateValidatorTest.php new file mode 100644 index 000000000000..2ff9730f6174 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/DateValidatorTest.php @@ -0,0 +1,78 @@ +validator = new DateValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new Date())); + } + + public function testExpectsStringCompatibleType() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid(new \stdClass(), new Date()); + } + + /** + * @dataProvider getValidDates + */ + public function testValidDates($date) + { + $this->assertTrue($this->validator->isValid($date, new Date())); + } + + public function getValidDates() + { + return array( + array('2010-01-01'), + array('1955-12-12'), + array('2030-05-31'), + ); + } + + /** + * @dataProvider getInvalidDates + */ + public function testInvalidDates($date) + { + $this->assertFalse($this->validator->isValid($date, new Date())); + } + + public function getInvalidDates() + { + return array( + array('foobar'), + array('2010-13-01'), + array('2010-04-32'), + array('2010-02-29'), + ); + } + + public function testMessageIsSet() + { + $constraint = new Date(array( + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid('foobar', $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => 'foobar', + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/EmailValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/EmailValidatorTest.php new file mode 100644 index 000000000000..a6f4b63f8abd --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/EmailValidatorTest.php @@ -0,0 +1,78 @@ +validator = new EmailValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new Email())); + } + + public function testExpectsStringCompatibleType() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid(new \stdClass(), new Email()); + } + + /** + * @dataProvider getValidEmails + */ + public function testValidEmails($email) + { + $this->assertTrue($this->validator->isValid($email, new Email())); + } + + public function getValidEmails() + { + return array( + array('fabien.potencier@symfony-project.com'), + array('example@example.co.uk'), + array('fabien_potencier@example.fr'), + ); + } + + /** + * @dataProvider getInvalidEmails + */ + public function testInvalidEmails($email) + { + $this->assertFalse($this->validator->isValid($email, new Email())); + } + + public function getInvalidEmails() + { + return array( + array('example'), + array('example@'), + array('example@localhost'), + array('example@example.com@example.com'), + ); + } + + public function testMessageIsSet() + { + $constraint = new Email(array( + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid('foobar', $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => 'foobar', + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/FileValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/FileValidatorTest.php new file mode 100644 index 000000000000..b376a0422718 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/FileValidatorTest.php @@ -0,0 +1,163 @@ +validator = new FileValidator(); + $this->path = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'FileValidatorTest'; + $this->file = fopen($this->path, 'w'); + } + + public function tearDown() + { + fclose($this->file); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new File())); + } + + public function testExpectsStringCompatibleTypeOrFile() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid(new \stdClass(), new File()); + } + + public function testValidFile() + { + $this->assertTrue($this->validator->isValid($this->path, new File())); + } + + public function testTooLargeBytes() + { + fwrite($this->file, str_repeat('0', 11)); + + $constraint = new File(array( + 'maxSize' => 10, + 'maxSizeMessage' => 'myMessage', + )); + + $this->assertFalse($this->validator->isValid($this->path, $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'limit' => '10 bytes', + 'size' => '11 bytes', + 'file' => $this->path, + )); + } + + public function testTooLargeKiloBytes() + { + fwrite($this->file, str_repeat('0', 1400)); + + $constraint = new File(array( + 'maxSize' => '1k', + 'maxSizeMessage' => 'myMessage', + )); + + $this->assertFalse($this->validator->isValid($this->path, $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'limit' => '1 kB', + 'size' => '1.4 kB', + 'file' => $this->path, + )); + } + + public function testTooLargeMegaBytes() + { + fwrite($this->file, str_repeat('0', 1400000)); + + $constraint = new File(array( + 'maxSize' => '1M', + 'maxSizeMessage' => 'myMessage', + )); + + $this->assertFalse($this->validator->isValid($this->path, $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'limit' => '1 MB', + 'size' => '1.4 MB', + 'file' => $this->path, + )); + } + + public function testInvalidMaxSize() + { + $constraint = new File(array( + 'maxSize' => '1abc', + )); + + $this->setExpectedException('Symfony\Components\Validator\Exception\ConstraintDefinitionException'); + + $this->validator->isValid($this->path, $constraint); + } + + public function testFileNotFound() + { + $constraint = new File(array( + 'notFoundMessage' => 'myMessage', + )); + + $this->assertFalse($this->validator->isValid('foobar', $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'file' => 'foobar', + )); + } + + public function testValidMimeType() + { + $file = $this->getMock('Symfony\Components\File\File', array(), array(), '', false); + $file->expects($this->any()) + ->method('getPath') + ->will($this->returnValue($this->path)); + $file->expects($this->any()) + ->method('getMimeType') + ->will($this->returnValue('image/jpg')); + + $constraint = new File(array( + 'mimeTypes' => array('image/png', 'image/jpg'), + )); + + $this->assertTrue($this->validator->isValid($file, $constraint)); + } + + public function testInvalidMimeType() + { + $file = $this->getMock('Symfony\Components\File\File', array(), array(), '', false); + $file->expects($this->any()) + ->method('getPath') + ->will($this->returnValue($this->path)); + $file->expects($this->any()) + ->method('getMimeType') + ->will($this->returnValue('application/pdf')); + + $constraint = new File(array( + 'mimeTypes' => array('image/png', 'image/jpg'), + 'mimeTypesMessage' => 'myMessage', + )); + + $this->assertFalse($this->validator->isValid($file, $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'type' => '"application/pdf"', + 'types' => '"image/png", "image/jpg"', + 'file' => $this->path, + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/MaxLengthValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/MaxLengthValidatorTest.php new file mode 100644 index 000000000000..dc5b8f9224e3 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/MaxLengthValidatorTest.php @@ -0,0 +1,89 @@ +validator = new MaxLengthValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new MaxLength(array('limit' => 5)))); + } + + public function testExpectsStringCompatibleType() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid(new \stdClass(), new MaxLength(array('limit' => 5))); + } + + /** + * @dataProvider getValidValues + */ + public function testValidValues($value, $skip = false) + { + if (!$skip) + { + $constraint = new MaxLength(array('limit' => 5)); + $this->assertTrue($this->validator->isValid($value, $constraint)); + } + } + + public function getValidValues() + { + return array( + array(12345), + array('12345'), + array('üüüüü', !function_exists('mb_strlen')), + array('ééééé', !function_exists('mb_strlen')), + ); + } + + /** + * @dataProvider getInvalidValues + */ + public function testInvalidValues($value, $skip = false) + { + if (!$skip) + { + $constraint = new MaxLength(array('limit' => 5)); + $this->assertFalse($this->validator->isValid($value, $constraint)); + } + } + + public function getInvalidValues() + { + return array( + array(123456), + array('123456'), + array('üüüüüü', !function_exists('mb_strlen')), + array('éééééé', !function_exists('mb_strlen')), + ); + } + + public function testMessageIsSet() + { + $constraint = new MaxLength(array( + 'limit' => 5, + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid('123456', $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => '123456', + 'limit' => 5, + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/MaxValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/MaxValidatorTest.php new file mode 100644 index 000000000000..c0e5ce549650 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/MaxValidatorTest.php @@ -0,0 +1,81 @@ +validator = new MaxValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new Max(array('limit' => 10)))); + } + + public function testExpectsNumericType() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid(new \stdClass(), new Max(array('limit' => 10))); + } + + /** + * @dataProvider getValidValues + */ + public function testValidValues($value) + { + $constraint = new Max(array('limit' => 10)); + $this->assertTrue($this->validator->isValid($value, $constraint)); + } + + public function getValidValues() + { + return array( + array(9.999999), + array(10), + array(10.0), + array('10'), + ); + } + + /** + * @dataProvider getInvalidValues + */ + public function testInvalidValues($value) + { + $constraint = new Max(array('limit' => 10)); + $this->assertFalse($this->validator->isValid($value, $constraint)); + } + + public function getInvalidValues() + { + return array( + array(10.00001), + array('10.00001'), + ); + } + + public function testMessageIsSet() + { + $constraint = new Max(array( + 'limit' => 10, + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid(11, $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => 11, + 'limit' => 10, + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/MinLengthValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/MinLengthValidatorTest.php new file mode 100644 index 000000000000..dd883a63f0be --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/MinLengthValidatorTest.php @@ -0,0 +1,89 @@ +validator = new MinLengthValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new MinLength(array('limit' => 6)))); + } + + public function testExpectsStringCompatibleType() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid(new \stdClass(), new MinLength(array('limit' => 5))); + } + + /** + * @dataProvider getValidValues + */ + public function testValidValues($value, $skip = false) + { + if (!$skip) + { + $constraint = new MinLength(array('limit' => 6)); + $this->assertTrue($this->validator->isValid($value, $constraint)); + } + } + + public function getValidValues() + { + return array( + array(123456), + array('123456'), + array('üüüüüü', !function_exists('mb_strlen')), + array('éééééé', !function_exists('mb_strlen')), + ); + } + + /** + * @dataProvider getInvalidValues + */ + public function testInvalidValues($value, $skip = false) + { + if (!$skip) + { + $constraint = new MinLength(array('limit' => 6)); + $this->assertFalse($this->validator->isValid($value, $constraint)); + } + } + + public function getInvalidValues() + { + return array( + array(12345), + array('12345'), + array('üüüüü', !function_exists('mb_strlen')), + array('ééééé', !function_exists('mb_strlen')), + ); + } + + public function testMessageIsSet() + { + $constraint = new MinLength(array( + 'limit' => 5, + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid('1234', $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => '1234', + 'limit' => 5, + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/MinValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/MinValidatorTest.php new file mode 100644 index 000000000000..cd2d20905b8c --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/MinValidatorTest.php @@ -0,0 +1,81 @@ +validator = new MinValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new Min(array('limit' => 10)))); + } + + public function testExpectsNumericType() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid(new \stdClass(), new Min(array('limit' => 10))); + } + + /** + * @dataProvider getValidValues + */ + public function testValidValues($value) + { + $constraint = new Min(array('limit' => 10)); + $this->assertTrue($this->validator->isValid($value, $constraint)); + } + + public function getValidValues() + { + return array( + array(10.00001), + array('10.00001'), + array(10), + array(10.0), + ); + } + + /** + * @dataProvider getInvalidValues + */ + public function testInvalidValues($value) + { + $constraint = new Min(array('limit' => 10)); + $this->assertFalse($this->validator->isValid($value, $constraint)); + } + + public function getInvalidValues() + { + return array( + array(9.999999), + array('9.999999'), + ); + } + + public function testMessageIsSet() + { + $constraint = new Min(array( + 'limit' => 10, + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid(9, $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => 9, + 'limit' => 10, + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/NotBlankValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/NotBlankValidatorTest.php new file mode 100644 index 000000000000..63ec4a816d14 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/NotBlankValidatorTest.php @@ -0,0 +1,57 @@ +validator = new NotBlankValidator(); + } + + /** + * @dataProvider getInvalidValues + */ + public function testInvalidValues($date) + { + $this->assertTrue($this->validator->isValid($date, new NotBlank())); + } + + public function getInvalidValues() + { + return array( + array('foobar'), + array(0), + array(false), + array(1234), + ); + } + + public function testNullIsInvalid() + { + $this->assertFalse($this->validator->isValid(null, new NotBlank())); + } + + public function testBlankIsInvalid() + { + $this->assertFalse($this->validator->isValid('', new NotBlank())); + } + + public function testSetMessage() + { + $constraint = new NotBlank(array( + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid('', $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/NotNullValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/NotNullValidatorTest.php new file mode 100644 index 000000000000..dc146925966e --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/NotNullValidatorTest.php @@ -0,0 +1,47 @@ +validator = new NotNullValidator(); + } + + /** + * @dataProvider getValidValues + */ + public function testValidValues($value) + { + $this->assertTrue($this->validator->isValid($value, new NotNull())); + } + + public function getValidValues() + { + return array( + array(0), + array(false), + array(true), + array(''), + ); + } + + public function testNullIsInvalid() + { + $constraint = new NotNull(array( + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid(null, $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array()); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/NullValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/NullValidatorTest.php new file mode 100644 index 000000000000..93e0fdf01a59 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/NullValidatorTest.php @@ -0,0 +1,54 @@ +validator = new NullValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new Null())); + } + + /** + * @dataProvider getInvalidValues + */ + public function testInvalidValues($value) + { + $this->assertFalse($this->validator->isValid($value, new Null())); + } + + public function getInvalidValues() + { + return array( + array(0), + array(false), + array(true), + array(''), + ); + } + + public function testSetMessage() + { + $constraint = new Null(array( + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid(1, $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => 1, + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/RegexValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/RegexValidatorTest.php new file mode 100644 index 000000000000..477910c3a2b9 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/RegexValidatorTest.php @@ -0,0 +1,80 @@ +validator = new RegexValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new Regex(array('pattern' => '/^[0-9]+$/')))); + } + + public function testExpectsStringCompatibleType() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid(new \stdClass(), new Regex(array('pattern' => '/^[0-9]+$/'))); + } + + /** + * @dataProvider getValidValues + */ + public function testValidValues($value) + { + $constraint = new Regex(array('pattern' => '/^[0-9]+$/')); + $this->assertTrue($this->validator->isValid($value, $constraint)); + } + + public function getValidValues() + { + return array( + array(0), + array('0'), + array('090909'), + array(90909), + ); + } + + /** + * @dataProvider getInvalidValues + */ + public function testInvalidValues($value) + { + $constraint = new Regex(array('pattern' => '/^[0-9]+$/')); + $this->assertFalse($this->validator->isValid($value, $constraint)); + } + + public function getInvalidValues() + { + return array( + array('abcd'), + array('090foo'), + ); + } + + public function testMessageIsSet() + { + $constraint = new Regex(array( + 'pattern' => '/^[0-9]+$/', + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid('foobar', $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => 'foobar', + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/TimeValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/TimeValidatorTest.php new file mode 100644 index 000000000000..9da89d4fc944 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/TimeValidatorTest.php @@ -0,0 +1,79 @@ +validator = new TimeValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new Time())); + } + + public function testExpectsStringCompatibleType() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid(new \stdClass(), new Time()); + } + + /** + * @dataProvider getValidTimes + */ + public function testValidTimes($time) + { + $this->assertTrue($this->validator->isValid($time, new Time())); + } + + public function getValidTimes() + { + return array( + array('01:02:03'), + array('00:00:00'), + array('23:59:59'), + ); + } + + /** + * @dataProvider getInvalidTimes + */ + public function testInvalidTimes($time) + { + $this->assertFalse($this->validator->isValid($time, new Time())); + } + + public function getInvalidTimes() + { + return array( + array('foobar'), + array('00:00'), + array('24:00:00'), + array('00:60:00'), + array('00:00:60'), + ); + } + + public function testMessageIsSet() + { + $constraint = new Time(array( + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid('foobar', $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => 'foobar', + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/UrlValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/UrlValidatorTest.php new file mode 100644 index 000000000000..879ba0569bff --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/UrlValidatorTest.php @@ -0,0 +1,82 @@ +validator = new UrlValidator(); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new Url())); + } + + public function testExpectsStringCompatibleType() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid(new \stdClass(), new Url()); + } + + /** + * @dataProvider getValidUrls + */ + public function testValidUrls($url) + { + $this->assertTrue($this->validator->isValid($url, new Url())); + } + + public function getValidUrls() + { + return array( + array('http://www.google.com'), + array('https://google.com/'), + array('https://google.com:80/'), + array('http://www.symfony-project.com/'), + array('http://127.0.0.1/'), + array('http://127.0.0.1:80/'), + array('ftp://google.com/foo.tgz'), + array('ftps://google.com/foo.tgz'), + ); + } + + /** + * @dataProvider getInvalidUrls + */ + public function testInvalidUrls($url) + { + $this->assertFalse($this->validator->isValid($url, new Url())); + } + + public function getInvalidUrls() + { + return array( + array('google.com'), + array('http:/google.com'), + array('http://google.com::aa'), + ); + } + + public function testMessageIsSet() + { + $constraint = new Url(array( + 'message' => 'myMessage' + )); + + $this->assertFalse($this->validator->isValid('foobar', $constraint)); + $this->assertEquals($this->validator->getMessageTemplate(), 'myMessage'); + $this->assertEquals($this->validator->getMessageParameters(), array( + 'value' => 'foobar', + )); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Constraints/ValidValidatorTest.php b/tests/Symfony/Tests/Components/Validator/Constraints/ValidValidatorTest.php new file mode 100644 index 000000000000..d0fdbf3d2a5b --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Constraints/ValidValidatorTest.php @@ -0,0 +1,107 @@ +walker = $this->getMock('Symfony\Components\Validator\GraphWalker', array(), array(), '', false); + $this->factory = $this->getMock('Symfony\Components\Validator\Mapping\ClassMetadataFactoryInterface'); + $messageInterpolator = $this->getMock('Symfony\Components\Validator\MessageInterpolator\MessageInterpolatorInterface'); + + $this->context = new ValidationContext('Root', $this->walker, $this->factory, $messageInterpolator); + + $this->validator = new ValidValidator(); + $this->validator->initialize($this->context); + } + + public function testNullIsValid() + { + $this->assertTrue($this->validator->isValid(null, new Valid())); + } + + public function testThrowsExceptionIfNotObjectOrArray() + { + $this->setExpectedException('Symfony\Components\Validator\Exception\UnexpectedTypeException'); + + $this->validator->isValid('foobar', new Valid()); + } + + public function testWalkObject() + { + $this->context->setGroup('MyGroup'); + $this->context->setPropertyPath('foo'); + + $metadata = $this->createClassMetadata(); + $entity = new Entity(); + + $this->factory->expects($this->once()) + ->method('getClassMetadata') + ->with($this->equalTo(self::CLASSNAME)) + ->will($this->returnValue($metadata)); + + $this->walker->expects($this->once()) + ->method('walkClass') + ->with($this->equalTo($metadata), $this->equalTo($entity), 'MyGroup', 'foo'); + + $this->assertTrue($this->validator->isValid($entity, new Valid())); + } + + public function testWalkArray() + { + $this->context->setGroup('MyGroup'); + $this->context->setPropertyPath('foo'); + + $constraint = new Valid(); + $entity = new Entity(); + // can only test for one object due to PHPUnit's mocking limitations + $array = array('key' => $entity); + + $this->walker->expects($this->once()) + ->method('walkConstraint') + ->with($this->equalTo($constraint), $this->equalTo($entity), 'MyGroup', 'foo[key]'); + + $this->assertTrue($this->validator->isValid($array, $constraint)); + } + + public function testValidateClass_Succeeds() + { + $metadata = $this->createClassMetadata(); + $entity = new Entity(); + + $this->factory->expects($this->any()) + ->method('getClassMetadata') + ->with($this->equalTo(self::CLASSNAME)) + ->will($this->returnValue($metadata)); + + $this->assertTrue($this->validator->isValid($entity, new Valid(array('class' => self::CLASSNAME)))); + } + + public function testValidateClass_Fails() + { + $entity = new \stdClass(); + + $this->assertFalse($this->validator->isValid($entity, new Valid(array('class' => self::CLASSNAME)))); + } + + protected function createClassMetadata() + { + return $this->getMock('Symfony\Components\Validator\Mapping\ClassMetadata', array(), array(), '', false); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/DependencyInjectionValidatorFactoryTest.php b/tests/Symfony/Tests/Components/Validator/DependencyInjectionValidatorFactoryTest.php new file mode 100644 index 000000000000..ea277c68c44d --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/DependencyInjectionValidatorFactoryTest.php @@ -0,0 +1,59 @@ +container = new Container(); + $this->factory = new DependencyInjectionValidatorFactory($this->container); + } + + public function tearDown() + { + unset ($this->factory); + unset ($this->container); + } + + public function testGetInstanceRetunsCorrectValidatorInstance() + { + $constraint = new Valid(); + $validator = $this->factory->getInstance($constraint); + $this->assertTrue($validator instanceof ValidValidator); + } + + public function testGetInstanceAddsValidatorServiceToContainer() + { + $constraint = new Valid(); + $validator = $this->factory->getInstance($constraint); + $this->assertServiceExists('Symfony.Components.Validator.Constraints.ValidValidator'); + } + + public function assertServiceExists($id) + { + $this->assertTrue($this->container->hasService($id), 'Service ' . $id . ' doesn\'t exist on container'); + } + + /** + * @expectedException LogicException + */ + public function testGetInstanceThrowsLogicExceptionIfValidatorNotInstanceOfValidatorInterface() + { + $constraint = new InvalidConstraint(); + $validator = $this->factory->getInstance($constraint); + } +} diff --git a/tests/Symfony/Tests/Components/Validator/Extension/DependencyInjectionValidatorFactoryTest.php b/tests/Symfony/Tests/Components/Validator/Extension/DependencyInjectionValidatorFactoryTest.php new file mode 100644 index 000000000000..587e0d233b30 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Extension/DependencyInjectionValidatorFactoryTest.php @@ -0,0 +1,59 @@ +container = new Container(); + $this->factory = new DependencyInjectionValidatorFactory($this->container); + } + + public function tearDown() + { + unset($this->factory); + unset($this->container); + } + + public function testGetInstanceRetunsCorrectValidatorInstance() + { + $constraint = new Valid(); + $validator = $this->factory->getInstance($constraint); + $this->assertTrue($validator instanceof ValidValidator); + } + + public function testGetInstanceAddsValidatorServiceToContainer() + { + $constraint = new Valid(); + $validator = $this->factory->getInstance($constraint); + $this->assertServiceExists('Symfony.Components.Validator.Constraints.ValidValidator'); + } + + private function assertServiceExists($id) + { + $this->assertTrue($this->container->hasService($id), 'Service ' . $id . ' doesn\'t exist on container'); + } + + /** + * @expectedException LogicException + */ + public function testGetInstanceThrowsLogicExceptionIfValidatorNotInstanceOfValidatorInterface() + { + $constraint = new InvalidConstraint(); + $validator = $this->factory->getInstance($constraint); + } +} diff --git a/tests/Symfony/Tests/Components/Validator/Fixtures/ConstraintA.php b/tests/Symfony/Tests/Components/Validator/Fixtures/ConstraintA.php new file mode 100644 index 000000000000..ebd4685312ab --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Fixtures/ConstraintA.php @@ -0,0 +1,16 @@ +setMessage('message', array('param' => 'value')); + return false; + } + + return true; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Fixtures/ConstraintB.php b/tests/Symfony/Tests/Components/Validator/Fixtures/ConstraintB.php new file mode 100644 index 000000000000..48fc498a630c --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Fixtures/ConstraintB.php @@ -0,0 +1,7 @@ +internal = $internal; + } + + public function getInternal() + { + return $this->internal . ' from getter'; + } + + /** + * @Validation({ + * @NotNull + * }) + */ + public function getLastName() + { + return $this->lastName; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Fixtures/EntityInterface.php b/tests/Symfony/Tests/Components/Validator/Fixtures/EntityInterface.php new file mode 100644 index 000000000000..d13321e162c3 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Fixtures/EntityInterface.php @@ -0,0 +1,7 @@ +loader = $loader; + parent::__construct($paths); + } + + protected function getFileLoaderInstance($file) + { + $this->timesCalled++; + return $this->loader; + } + + public function getTimesCalled() + { + return $this->timesCalled; + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Fixtures/InvalidConstraint.php b/tests/Symfony/Tests/Components/Validator/Fixtures/InvalidConstraint.php new file mode 100644 index 000000000000..24d85496046a --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Fixtures/InvalidConstraint.php @@ -0,0 +1,7 @@ +interpolator = $this->getMock('Symfony\Components\Validator\MessageInterpolator\MessageInterpolatorInterface'); + $this->factory = $this->getMock('Symfony\Components\Validator\Mapping\ClassMetadataFactoryInterface'); + $this->walker = new GraphWalker('Root', $this->factory, new ConstraintValidatorFactory(), $this->interpolator); + $this->metadata = new ClassMetadata(self::CLASSNAME); + } + + public function testWalkClassValidatesConstraints() + { + $this->metadata->addConstraint(new ConstraintA()); + + $this->walker->walkClass($this->metadata, new Entity(), 'Default', ''); + + $this->assertEquals(1, count($this->walker->getViolations())); + } + + public function testWalkClassValidatesPropertyConstraints() + { + $this->metadata->addPropertyConstraint('firstName', new ConstraintA()); + + $this->walker->walkClass($this->metadata, new Entity(), 'Default', ''); + + $this->assertEquals(1, count($this->walker->getViolations())); + } + + public function testWalkClassValidatesGetterConstraints() + { + $this->metadata->addGetterConstraint('lastName', new ConstraintA()); + + $this->walker->walkClass($this->metadata, new Entity(), 'Default', ''); + + $this->assertEquals(1, count($this->walker->getViolations())); + } + + public function testWalkPropertyValueValidatesConstraints() + { + $this->metadata->addPropertyConstraint('firstName', new ConstraintA()); + + $this->walker->walkPropertyValue($this->metadata, 'firstName', 'value', 'Default', ''); + + $this->assertEquals(1, count($this->walker->getViolations())); + } + + public function testWalkConstraintBuildsAViolationIfFailed() + { + $constraint = new ConstraintA(); + + $this->interpolator->expects($this->once()) + ->method('interpolate') + ->with($this->equalTo('message'), $this->equalTo(array('param' => 'value'))) + ->will($this->returnValue('interpolated text')); + + $this->walker->walkConstraint($constraint, 'foobar', 'Default', 'firstName.path'); + + $violations = new ConstraintViolationList(); + $violations->add(new ConstraintViolation( + 'interpolated text', + 'Root', + 'firstName.path', + 'foobar' + )); + + $this->assertEquals($violations, $this->walker->getViolations()); + } + + public function testWalkConstraintBuildsNoViolationIfSuccessful() + { + $constraint = new ConstraintA(); + + $this->walker->walkConstraint($constraint, 'VALID', 'Default', 'firstName.path'); + + $this->assertEquals(0, count($this->walker->getViolations())); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Mapping/ClassMetadataFactoryTest.php b/tests/Symfony/Tests/Components/Validator/Mapping/ClassMetadataFactoryTest.php new file mode 100644 index 000000000000..8c45be133cad --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Mapping/ClassMetadataFactoryTest.php @@ -0,0 +1,67 @@ +getClassMetadata(self::PARENTCLASS); + + $constraints = array( + new ConstraintA(array('groups' => array('Default', 'EntityParent'))), + ); + + $this->assertEquals($constraints, $metadata->getConstraints()); + } + + public function testMergeParentConstraints() + { + $factory = new ClassMetadataFactory(new TestLoader()); + $metadata = $factory->getClassMetadata(self::CLASSNAME); + + $constraints = array( + new ConstraintA(array('groups' => array( + 'Default', + 'EntityParent', + 'Entity', + ))), + new ConstraintA(array('groups' => array( + 'Default', + 'EntityInterface', + 'Entity', + ))), + new ConstraintA(array('groups' => array( + 'Default', + 'Entity', + ))), + ); + + $this->assertEquals($constraints, $metadata->getConstraints()); + } +} + +class TestLoader implements LoaderInterface +{ + public function loadClassMetadata(ClassMetadata $metadata) + { + $metadata->addConstraint(new ConstraintA()); + } +} diff --git a/tests/Symfony/Tests/Components/Validator/Mapping/ClassMetadataTest.php b/tests/Symfony/Tests/Components/Validator/Mapping/ClassMetadataTest.php new file mode 100644 index 000000000000..24e9b0bf57af --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Mapping/ClassMetadataTest.php @@ -0,0 +1,124 @@ +metadata = new ClassMetadata(self::CLASSNAME); + } + + public function testAddPropertyConstraints() + { + $this->metadata->addPropertyConstraint('firstName', new ConstraintA()); + $this->metadata->addPropertyConstraint('lastName', new ConstraintB()); + + $this->assertEquals(array('firstName', 'lastName'), $this->metadata->getConstrainedProperties()); + } + + public function testMergeConstraintsMergesClassConstraints() + { + $parent = new ClassMetadata(self::PARENTCLASS); + $parent->addConstraint(new ConstraintA()); + + $this->metadata->mergeConstraints($parent); + $this->metadata->addConstraint(new ConstraintA()); + + $constraints = array( + new ConstraintA(array('groups' => array( + 'Default', + 'EntityParent', + 'Entity', + ))), + new ConstraintA(array('groups' => array( + 'Default', + 'Entity', + ))), + ); + + $this->assertEquals($constraints, $this->metadata->getConstraints()); + } + + public function testMergeConstraintsMergesMemberConstraints() + { + $parent = new ClassMetadata(self::PARENTCLASS); + $parent->addPropertyConstraint('firstName', new ConstraintA()); + + $this->metadata->mergeConstraints($parent); + $this->metadata->addPropertyConstraint('firstName', new ConstraintA()); + + $constraints = array( + new ConstraintA(array('groups' => array( + 'Default', + 'EntityParent', + 'Entity', + ))), + new ConstraintA(array('groups' => array( + 'Default', + 'Entity', + ))), + ); + + $members = $this->metadata->getMemberMetadatas('firstName'); + + $this->assertEquals(1, count($members)); + $this->assertEquals(self::PARENTCLASS, $members[0]->getClassName()); + $this->assertEquals($constraints, $members[0]->getConstraints()); + } + + public function testMergeConstraintsKeepsPrivateMembersSeperate() + { + $parent = new ClassMetadata(self::PARENTCLASS); + $parent->addPropertyConstraint('internal', new ConstraintA()); + + $this->metadata->mergeConstraints($parent); + $this->metadata->addPropertyConstraint('internal', new ConstraintA()); + + $parentConstraints = array( + new ConstraintA(array('groups' => array( + 'Default', + 'EntityParent', + 'Entity', + ))), + ); + $constraints = array( + new ConstraintA(array('groups' => array( + 'Default', + 'Entity', + ))), + ); + + $members = $this->metadata->getMemberMetadatas('internal'); + + $this->assertEquals(2, count($members)); + $this->assertEquals(self::PARENTCLASS, $members[0]->getClassName()); + $this->assertEquals($parentConstraints, $members[0]->getConstraints()); + $this->assertEquals(self::CLASSNAME, $members[1]->getClassName()); + $this->assertEquals($constraints, $members[1]->getConstraints()); + } + + public function testGetReflectionClass() + { + $reflClass = new \ReflectionClass(self::CLASSNAME); + + $this->assertEquals($reflClass, $this->metadata->getReflectionClass()); + } +} + diff --git a/tests/Symfony/Tests/Components/Validator/Mapping/ElementMetadataTest.php b/tests/Symfony/Tests/Components/Validator/Mapping/ElementMetadataTest.php new file mode 100644 index 000000000000..fb069f5bb9c2 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Mapping/ElementMetadataTest.php @@ -0,0 +1,51 @@ +metadata = new ElementMetadata('Symfony\Tests\Components\Validator\Fixtures\Entity'); + } + + public function testAddConstraints() + { + $this->metadata->addConstraint($constraint1 = new ConstraintA()); + $this->metadata->addConstraint($constraint2 = new ConstraintA()); + + $this->assertEquals(array($constraint1, $constraint2), $this->metadata->getConstraints()); + } + + public function testMultipleConstraintsOfTheSameType() + { + $constraint1 = new ConstraintA(array('property1' => 'A')); + $constraint2 = new ConstraintA(array('property1' => 'B')); + + $this->metadata->addConstraint($constraint1); + $this->metadata->addConstraint($constraint2); + + $this->assertEquals(array($constraint1, $constraint2), $this->metadata->getConstraints()); + } + + public function testFindConstraintsByGroup() + { + $constraint1 = new ConstraintA(array('groups' => 'TestGroup')); + $constraint2 = new ConstraintB(); + + $this->metadata->addConstraint($constraint1); + $this->metadata->addConstraint($constraint2); + + $this->assertEquals(array($constraint1), $this->metadata->findConstraints('TestGroup')); + } +} diff --git a/tests/Symfony/Tests/Components/Validator/Mapping/GetterMetadataTest.php b/tests/Symfony/Tests/Components/Validator/Mapping/GetterMetadataTest.php new file mode 100644 index 000000000000..fb660bf49024 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Mapping/GetterMetadataTest.php @@ -0,0 +1,33 @@ +setExpectedException('Symfony\Components\Validator\Exception\ValidatorException'); + + new GetterMetadata(self::CLASSNAME, 'foobar'); + } + + public function testGetValueFromPublicGetter() + { + // private getters don't work yet because ReflectionMethod::setAccessible() + // does not exists yet in a stable PHP release + + $entity = new Entity('foobar'); + $metadata = new GetterMetadata(self::CLASSNAME, 'internal'); + + $this->assertEquals('foobar from getter', $metadata->getValue($entity)); + } +} + diff --git a/tests/Symfony/Tests/Components/Validator/Mapping/Loader/AnnotationLoaderTest.php b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/AnnotationLoaderTest.php new file mode 100644 index 000000000000..118e3d877452 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/AnnotationLoaderTest.php @@ -0,0 +1,62 @@ +assertTrue($loader->loadClassMetadata($metadata)); + } + + public function testLoadClassMetadataReturnsFalseIfNotSuccessful() + { + $loader = new AnnotationLoader(); + $metadata = new ClassMetadata('\stdClass'); + + $this->assertFalse($loader->loadClassMetadata($metadata)); + } + + public function testLoadClassMetadata() + { + $loader = new AnnotationLoader(); + $metadata = new ClassMetadata('Symfony\Tests\Components\Validator\Fixtures\Entity'); + + $loader->loadClassMetadata($metadata); + + $expected = new ClassMetadata('Symfony\Tests\Components\Validator\Fixtures\Entity'); + $expected->addConstraint(new NotNull()); + $expected->addConstraint(new Min(3)); + $expected->addConstraint(new Choice(array('A', 'B'))); + $expected->addConstraint(new All(array(new NotNull(), new Min(3)))); + $expected->addConstraint(new All(array('constraints' => array(new NotNull(), new Min(3))))); + $expected->addConstraint(new Collection(array('fields' => array( + 'foo' => array(new NotNull(), new Min(3)), + 'bar' => new Min(5), + )))); + $expected->addPropertyConstraint('firstName', new Choice(array( + 'message' => 'Must be one of %choices%', + 'choices' => array('A', 'B'), + ))); + $expected->addGetterConstraint('lastName', new NotNull()); + + // load reflection class so that the comparison passes + $expected->getReflectionClass(); + + $this->assertEquals($expected, $metadata); + } +} diff --git a/tests/Symfony/Tests/Components/Validator/Mapping/Loader/FilesLoaderTest.php b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/FilesLoaderTest.php new file mode 100644 index 000000000000..35a67dd022e0 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/FilesLoaderTest.php @@ -0,0 +1,43 @@ +getFilesLoader($this->getFileLoader()); + $this->assertEquals(4, $loader->getTimesCalled()); + } + + public function testCallsActualFileLoaderForMetadata() + { + $fileLoader = $this->getFileLoader(); + $fileLoader->expects($this->exactly(4)) + ->method('loadClassMetadata'); + $loader = $this->getFilesLoader($fileLoader); + $loader->loadClassMetadata(new ClassMetadata('Symfony\Tests\Components\Validator\Fixtures\Entity')); + } + + public function getFilesLoader(LoaderInterface $loader) + { + return $this->getMockForAbstractClass('Symfony\Tests\Components\Validator\Fixtures\FilesLoader', array(array( + __DIR__ . '/constraint-mapping.xml', + __DIR__ . '/constraint-mapping.yaml', + __DIR__ . '/constraint-mapping.test', + __DIR__ . '/constraint-mapping.txt', + ), $loader)); + } + + public function getFileLoader() + { + return $this->getMock('Symfony\Components\Validator\Mapping\Loader\LoaderInterface'); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Mapping/Loader/LoaderChainTest.php b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/LoaderChainTest.php new file mode 100644 index 000000000000..c05002938266 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/LoaderChainTest.php @@ -0,0 +1,78 @@ +getMock('Symfony\Components\Validator\Mapping\Loader\LoaderInterface'); + $loader1->expects($this->once()) + ->method('loadClassMetadata') + ->with($this->equalTo($metadata)); + + $loader2 = $this->getMock('Symfony\Components\Validator\Mapping\Loader\LoaderInterface'); + $loader2->expects($this->once()) + ->method('loadClassMetadata') + ->with($this->equalTo($metadata)); + + $chain = new LoaderChain(array( + $loader1, + $loader2, + )); + + $chain->loadClassMetadata($metadata); + } + + public function testReturnsTrueIfAnyLoaderReturnedTrue() + { + $metadata = new ClassMetadata('\stdClass'); + + $loader1 = $this->getMock('Symfony\Components\Validator\Mapping\Loader\LoaderInterface'); + $loader1->expects($this->any()) + ->method('loadClassMetadata') + ->will($this->returnValue(true)); + + $loader2 = $this->getMock('Symfony\Components\Validator\Mapping\Loader\LoaderInterface'); + $loader2->expects($this->any()) + ->method('loadClassMetadata') + ->will($this->returnValue(false)); + + $chain = new LoaderChain(array( + $loader1, + $loader2, + )); + + $this->assertTrue($chain->loadClassMetadata($metadata)); + } + + public function testReturnsFalseIfNoLoaderReturnedTrue() + { + $metadata = new ClassMetadata('\stdClass'); + + $loader1 = $this->getMock('Symfony\Components\Validator\Mapping\Loader\LoaderInterface'); + $loader1->expects($this->any()) + ->method('loadClassMetadata') + ->will($this->returnValue(false)); + + $loader2 = $this->getMock('Symfony\Components\Validator\Mapping\Loader\LoaderInterface'); + $loader2->expects($this->any()) + ->method('loadClassMetadata') + ->will($this->returnValue(false)); + + $chain = new LoaderChain(array( + $loader1, + $loader2, + )); + + $this->assertFalse($chain->loadClassMetadata($metadata)); + } +} + diff --git a/tests/Symfony/Tests/Components/Validator/Mapping/Loader/StaticMethodLoaderTest.php b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/StaticMethodLoaderTest.php new file mode 100644 index 000000000000..9d55e732d844 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/StaticMethodLoaderTest.php @@ -0,0 +1,47 @@ +assertTrue($loader->loadClassMetadata($metadata)); + } + + public function testLoadClassMetadataReturnsFalseIfNotSuccessful() + { + $loader = new StaticMethodLoader('loadMetadata'); + $metadata = new ClassMetadata('\stdClass'); + + $this->assertFalse($loader->loadClassMetadata($metadata)); + } + + public function testLoadClassMetadata() + { + $loader = new StaticMethodLoader('loadMetadata'); + $metadata = new ClassMetadata(__NAMESPACE__.'\StaticLoaderEntity'); + + $loader->loadClassMetadata($metadata); + + $this->assertEquals(StaticLoaderEntity::$invokedWith, $metadata); + } +} + +class StaticLoaderEntity +{ + static public $invokedWith = null; + + public static function loadMetadata(ClassMetadata $metadata) + { + self::$invokedWith = $metadata; + } +} diff --git a/tests/Symfony/Tests/Components/Validator/Mapping/Loader/XmlFileLoaderTest.php b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/XmlFileLoaderTest.php new file mode 100644 index 000000000000..d804611596ff --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/XmlFileLoaderTest.php @@ -0,0 +1,59 @@ +assertTrue($loader->loadClassMetadata($metadata)); + } + + public function testLoadClassMetadataReturnsFalseIfNotSuccessful() + { + $loader = new XmlFileLoader(__DIR__.'/constraint-mapping.xml'); + $metadata = new ClassMetadata('\stdClass'); + + $this->assertFalse($loader->loadClassMetadata($metadata)); + } + + public function testLoadClassMetadata() + { + $loader = new XmlFileLoader(__DIR__.'/constraint-mapping.xml'); + $metadata = new ClassMetadata('Symfony\Tests\Components\Validator\Fixtures\Entity'); + + $loader->loadClassMetadata($metadata); + + $expected = new ClassMetadata('Symfony\Tests\Components\Validator\Fixtures\Entity'); + $expected->addConstraint(new NotNull()); + $expected->addConstraint(new Min(3)); + $expected->addConstraint(new Choice(array('A', 'B'))); + $expected->addConstraint(new All(array(new NotNull(), new Min(3)))); + $expected->addConstraint(new All(array('constraints' => array(new NotNull(), new Min(3))))); + $expected->addConstraint(new Collection(array('fields' => array( + 'foo' => array(new NotNull(), new Min(3)), + 'bar' => array(new Min(5)), + )))); + $expected->addPropertyConstraint('firstName', new Choice(array( + 'message' => 'Must be one of %choices%', + 'choices' => array('A', 'B'), + ))); + $expected->addGetterConstraint('lastName', new NotNull()); + + $this->assertEquals($expected, $metadata); + } +} diff --git a/tests/Symfony/Tests/Components/Validator/Mapping/Loader/YamlFileLoaderTest.php b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/YamlFileLoaderTest.php new file mode 100644 index 000000000000..ff5072998551 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/YamlFileLoaderTest.php @@ -0,0 +1,59 @@ +assertTrue($loader->loadClassMetadata($metadata)); + } + + public function testLoadClassMetadataReturnsFalseIfNotSuccessful() + { + $loader = new YamlFileLoader(__DIR__.'/constraint-mapping.yml'); + $metadata = new ClassMetadata('\stdClass'); + + $this->assertFalse($loader->loadClassMetadata($metadata)); + } + + public function testLoadClassMetadata() + { + $loader = new YamlFileLoader(__DIR__.'/constraint-mapping.yml'); + $metadata = new ClassMetadata('Symfony\Tests\Components\Validator\Fixtures\Entity'); + + $loader->loadClassMetadata($metadata); + + $expected = new ClassMetadata('Symfony\Tests\Components\Validator\Fixtures\Entity'); + $expected->addConstraint(new NotNull()); + $expected->addConstraint(new Min(3)); + $expected->addConstraint(new Choice(array('A', 'B'))); + $expected->addConstraint(new All(array(new NotNull(), new Min(3)))); + $expected->addConstraint(new All(array('constraints' => array(new NotNull(), new Min(3))))); + $expected->addConstraint(new Collection(array('fields' => array( + 'foo' => array(new NotNull(), new Min(3)), + 'bar' => array(new Min(5)), + )))); + $expected->addPropertyConstraint('firstName', new Choice(array( + 'message' => 'Must be one of %choices%', + 'choices' => array('A', 'B'), + ))); + $expected->addGetterConstraint('lastName', new NotNull()); + + $this->assertEquals($expected, $metadata); + } +} diff --git a/tests/Symfony/Tests/Components/Validator/Mapping/Loader/constraint-mapping.xml b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/constraint-mapping.xml new file mode 100644 index 000000000000..43eff44c75cb --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/constraint-mapping.xml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + 3 + + + + A + B + + + + + + 3 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Mapping/Loader/constraint-mapping.yml b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/constraint-mapping.yml new file mode 100644 index 000000000000..2a8ae2963c61 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Mapping/Loader/constraint-mapping.yml @@ -0,0 +1,35 @@ +Symfony\Tests\Components\Validator\Fixtures\Entity: + constraints: + # Constraint without value + - NotNull: ~ + # Constraint with single value + - Min: 3 + # Constraint with multiple values + - Choice: [A, B] + # Constraint with child constraints + - All: + - NotNull: ~ + - Min: 3 + # Option with child constraints + - All: + constraints: + - NotNull: ~ + - Min: 3 + # Value with child constraints + - Collection: + fields: + foo: + - NotNull: ~ + - Min: 3 + bar: + - Min: 5 + + properties: + firstName: + # Constraint with options + - Choice: { choices: [A, B], message: Must be one of %choices% } + + getters: + lastName: + - NotNull: ~ + \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/Mapping/PropertyMetadataTest.php b/tests/Symfony/Tests/Components/Validator/Mapping/PropertyMetadataTest.php new file mode 100644 index 000000000000..c56fd69d668f --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/Mapping/PropertyMetadataTest.php @@ -0,0 +1,30 @@ +setExpectedException('Symfony\Components\Validator\Exception\ValidatorException'); + + new PropertyMetadata(self::CLASSNAME, 'foobar'); + } + + public function testGetValueFromPrivateProperty() + { + $entity = new Entity('foobar'); + $metadata = new PropertyMetadata(self::CLASSNAME, 'internal'); + + $this->assertEquals('foobar', $metadata->getValue($entity)); + } +} + diff --git a/tests/Symfony/Tests/Components/Validator/MessageInterpolator/XliffMessageInterpolatorTest.php b/tests/Symfony/Tests/Components/Validator/MessageInterpolator/XliffMessageInterpolatorTest.php new file mode 100644 index 000000000000..3b0bccd76909 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/MessageInterpolator/XliffMessageInterpolatorTest.php @@ -0,0 +1,41 @@ +interpolate('original', array('param' => 'foobar')); + + $this->assertEquals('translation with param foobar', $text); + } + + public function testInterpolateFromMultipleFiles() + { + $interpolator = new XliffMessageInterpolator(array( + __DIR__.'/xliff.xml', + __DIR__.'/xliff2.xml', + )); + + $text1 = $interpolator->interpolate('original', array('param' => 'foobar')); + $text2 = $interpolator->interpolate('second', array('param' => 'baz')); + + $this->assertEquals('translation with param foobar', $text1); + $this->assertEquals('second translation with param baz', $text2); + } + + public function testConvertParamsToStrings() + { + $interpolator = new XliffMessageInterpolator(__DIR__.'/xliff.xml'); + $text = $interpolator->interpolate('original', array('param' => array())); + + $this->assertEquals('translation with param Array', $text); + } +} + diff --git a/tests/Symfony/Tests/Components/Validator/MessageInterpolator/xliff.xml b/tests/Symfony/Tests/Components/Validator/MessageInterpolator/xliff.xml new file mode 100644 index 000000000000..1e4a4f0e7fb1 --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/MessageInterpolator/xliff.xml @@ -0,0 +1,11 @@ + + + + + + original + translation with param %param% + + + + \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/MessageInterpolator/xliff2.xml b/tests/Symfony/Tests/Components/Validator/MessageInterpolator/xliff2.xml new file mode 100644 index 000000000000..0d6ba385d38e --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/MessageInterpolator/xliff2.xml @@ -0,0 +1,11 @@ + + + + + + second + second translation with param %param% + + + + \ No newline at end of file diff --git a/tests/Symfony/Tests/Components/Validator/ValidatorTest.php b/tests/Symfony/Tests/Components/Validator/ValidatorTest.php new file mode 100644 index 000000000000..cf585e333dbe --- /dev/null +++ b/tests/Symfony/Tests/Components/Validator/ValidatorTest.php @@ -0,0 +1,83 @@ +getMock('Symfony\Components\Validator\ConstraintValidatorInterface'); + $validatorMock->expects($this->once()) + ->method('isValid') + ->with($this->equalTo('Bernhard'), $this->equalTo($constraint)) + ->will($this->returnValue(false)); + $validatorMock->expects($this->atLeastOnce()) + ->method('getMessageTemplate') + ->will($this->returnValue('message')); + $validatorMock->expects($this->atLeastOnce()) + ->method('getMessageParameters') + ->will($this->returnValue(array('param' => 'value'))); + + $factoryMock = $this->getMock('Symfony\Components\Validator\ConstraintValidatorFactoryInterface'); + $factoryMock->expects($this->once()) + ->method('getInstance') + ->with($this->equalTo($constraint->validatedBy())) + ->will($this->returnValue($validatorMock)); + + $validator = new Validator($metadata, $factoryMock); + + $builder = new PropertyPathBuilder(); + $expected = new ConstraintViolationList(); + $expected->add(new ConstraintViolation( + 'message', + array('param' => 'value'), + $subjectClass, + $builder->atProperty('firstName')->getPropertyPath(), + 'Bernhard' + )); + + $this->assertEquals($expected, $validator->validateProperty($subject, 'firstName')); + */ + } +} \ No newline at end of file