I uploaded this document to github. You can find it under http://github.com/graste/Agavi-FAQ.
This unofficial FAQ is a work in progress. All information is provided as is without any guarantees. Use
it at your own risk. Oh, and beware of that yellow fade animated gif in the background. Instant eye cancer. ;-)
On this page a few common questions are answered. Examples are given where appropriate. If you have questions that are not answered here, visit the user mailing list, join the IRC channel or search through mailing list archive or IRC channel logs as a start. The Agavi source code is well documented and the sample application should give you some good practices to start with. In case you want a question added here, ask via mail (agavi-faq@mivesto.de) or visit the IRC channel (ask graste).
The examples in this FAQ are skeletons most of the time. It is recommended to be strict when checking user input such as header variables, cookies, files or request parameters. The validator examples are just there to get you started with some features. The usage of fragment caching may lead to side effects if you define caches only for production environment and then begin to wonder why some code or slot or whatever doesn't get executed as it always was before. Please think twice before copying and pasting example code (which is not production ready on itself most of the time).
Most good practices or patterns are probably to be found in the sample application or the official Agavi quick start tutorial.
With Agavi you can have different contexts (think 'web', 'console' or 'soap') and multiple environments
(like 'development', 'staging', 'production' etc.). The <ae:configuration>
sections
in the config files are sorted first (vanilla, environment-only, context-only, environment+context) and
then merged together. Knowing this, you can just define default settings and then overwrite them
in context- or environment-specific sections. Some examples below make use of this. You may use regular expressions
like this in the attribute: environment="development.*"
. This would allow for common settings for
all environments that start with development
(so John and Chuck from your dev-team get those
settings in their private development-john-rambo
or development-chuck-norris
configuration sections).
isSimple()
method all about (in an action)?HTTP_*
variables?AgaviImageFileValidator
?isSet
or isNotEmpty
to validate route parameters?inarray
validator?AgaviDateTimeValidator
?source
attribute values are available and what do they do?$favoriteIDE
?executeCreate()
with HTTP PUT, executeWrite()
with HTTP POST, executeRead()
with HTTP GET and executeRemove()
with
HTTP DELETE. If you want to change these mappings modify the app/config/factories.xml
file and add the following:
<request class="AgaviWebRequest"> <ae:parameter name="method_names"> <ae:parameter name="POST">create<ae:parameter> <ae:parameter name="GET">read<ae:parameter> <ae:parameter name="PUT">write<ae:parameter> <ae:parameter name="DELETE">remove<ae:parameter> <ae:parameter> <request>The above mappings are the default ones defined by Agavi. Please note, that you may use any arbitrary request method you want to support.
To support uncommon or custom request methods use a specific execute[Requestmethod]()
in your
actions to react to different specific request methods OR use the execute()
method for any request method. Let's say you
want your application to react to OPTIONS
or HEAD
requests: Just create executeOptions()
and
executeHead()
in your action and be done. Remember, that you can only get validated parameters, headers, files
and cookies. To test your executeHead()/executeOptions()
you may use something like
curl -I http://route/to/action
or curl -X OPTIONS http://route/to/action
.
$this->getLayer('content')->setExtension($whatever);
or do this in the config
or set the default_extension parameter of the renderer. You could set a
<parameter name="extension">.whatever</parameter>
to the relevant
layer as setExtension()
is just a __call()
overload for setParameter('.extension')
.
↑isSimple()
method all about (in an action)?isSimple()
method determines whether the actions' validate*
and execute*
methods are called. If isSimple()
returns true
,
action validation and execution are bypassed and the default view (see getDefaultView()
)
is executed. Note also, that no filters are run (not even the security filter) and
no request data is available except for arguments explicitly passed to the container.public function isSimple() { return (bool)$this->getContainer()->getParameter('is_slot'); // only simple when run as slot }Remember, that you have to validate parameters given to an action, if that action's not simple (and you only get parameters if there's at least an
execute*()
method).
↑output_types.xml
file:
<!-- include common layer definitions from the sandbox element --> <xi:include xpointer="xmlns(ae=http://agavi.org/agavi/config/global/envelope/1.0) xmlns(ot=http://agavi.org/agavi/config/parts/output_types/1.0) xpointer(/ae:configurations/ae:sandbox/ot:layers/*)" />↑
<output_type exception_template="...">Default exception templates and custom ones per context may be set via
settings.xml
file. Customizations according
to the needs of your application are possible per environment, context and output type:
<!-- in settings.xml --> <exception_templates> <!-- this is the exception template that's used by default unless one for a specific context has been defined, or for the current output type --> <!-- note that exceptions that occur before a context is even fully initialized, or an exception that happens before the output types are loaded and determined, will use a template defined here --> <exception_template>%core.agavi_dir%/exception/templates/shiny.php</exception_template> <!-- an example for per-context exception templates --> <!-- per-output-type templates can be set in output_types.xml --> <exception_template context="console">%core.agavi_dir%/exception/templates/plaintext.php</exception_template> </exception_templates> <!-- in output_types.xml --> <ae:configuration environment="production.*"> <output_types default="html"> <!-- use a different exception template in production environments for HTML --> <output_type name="html" exception_template="%core.template_dir%/exceptions/web-html.php" /> </output_types> </ae:configuration>This makes it possibles to have the verbose shiny exception template in development while using simple HTTP 500 status error pages in production. ↑
app/config/settings.xml
.
You can define different settings and values for all environments and contexts you use.
<ae:configuration> <settings prefix="Blog."> <setting name="enabled">true</setting> <setting name="EntriesPerPage">25</setting> <setting name="Theme">green</setting> <setting name="More"> <ae:parameters> <ae:parameter name="foo">bar</ae:parameter> <ae:parameter name="blah">blub</ae:parameter> </ae:parameters> </setting> </settings> </ae:configuration> <ae:configuration environment="production"> <settings prefix="Blog."> <setting name="EntriesPerPage">10</setting> </settings> </ae:configuration>You then get values like this:
$entries_per_page = AgaviConfig::get('Blog.EntriesPerPage', $default_value_if_not_defined);Getting the
Blog.More
settings entry will result in an indexed array you can use.module.xml
files to store values. You access these settings like this:
AgaviConfig::get('modules.lowercase_module_name.SomeSetting');Remember, that you can always set values programatically using:
AgaviConfig::set('some.setting', 'some_value');as you can see in you application's
index.php
or config.php
files. Another thing to
know about the different settings in different contexts and environments is the
following: <ae:configuration>
sections are sorted first (vanilla, environment-only,
context-only, environment+context) and then merged together. Knowing this you can just define
default settings and then overwrite them in context- or environment-specific sections like
in the example above.
↑app/config/database.xml
file (see that settings
parameter):
<ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" xmlns="http://agavi.org/agavi/config/parts/databases/1.0"> <ae:configuration environment="development"> <databases default="db"> <database name="propel" class="AgaviPropelDatabase"> <ae:parameter name="config">%core.app_dir%/config/propelproject-conf.php</ae:parameter> <ae:parameter name="overrides"> <ae:parameter name="connection"> <ae:parameter name="dsn">mysql:dbname=icanhazdb;host=127.0.0.1</ae:parameter> <ae:parameter name="user">w00t</ae:parameter> <ae:parameter name="password">not_default_pass</ae:parameter> <ae:parameter name="settings"> <ae:parameter name="charset"> <ae:parameter name="value">utf8</ae:parameter> </ae:parameter> </ae:parameter> </ae:parameter> </ae:parameter> </database> <database name="db" class="AgaviDoctrineDatabase"> <ae:parameters> <ae:parameter name="attributes"> <ae:parameters> <ae:parameter name="Doctrine_Core::ATTR_AUTOLOAD_TABLE_CLASSES">true</ae:parameter> <ae:parameter name="Doctrine::ATTR_USE_NATIVE_ENUM">true</ae:parameter> <ae:parameter name="Doctrine_Core::ATTR_VALIDATE">LENGTHS</ae:parameter> <ae:parameter name="Doctrine_Core::ATTR_AUTO_ACCESSOR_OVERRIDE">true</ae:parameter> <ae:parameter name="Doctrine_Core::ATTR_QUOTE_IDENTIFIER">true</ae:parameter> </ae:parameters> </ae:parameter> <ae:parameter name="manager_attributes"> <ae:parameters> <ae:parameter name="Doctrine_Core::ATTR_MODEL_LOADING">2</ae:parameter> </ae:parameters> </ae:parameter> <ae:parameter name="load_models">%core.lib_dir%/orm</ae:parameter> <ae:parameter name="charset">UTF8</ae:parameter> </ae:parameters> </database> </databases> </ae:configuration> <!-- Simple example on how to include e.g. more sensitive database settings from an external file that may have been generated on deployment or similar and contains only things like hostname, db name, port, username and password. --> <xi:include href="%core.app_dir%/../etc/local.databases.xml" xpointer="xmlns(ae=http://agavi.org/agavi/config/global/envelope/1.0) xpointer(/ae:configurations/*)"> </xi:include> </ae:configurations>This example's taken from DracoBlue's blog entry and extended by a bit of XInclude magic and Doctrine settings. ↑
HTTP_*
variables?AgaviWebRequestDataHolder
defines headers
, files
and cookies
as sources (apart from GET/POST parameters, which you simply access
by validating them and then using $rd->getParameter('name', $default)
).headers
source contains all HTTP_*
variables you might want to use. The
only thing between you and your values is strict validation, as these settings are
user provided request data and therefore unvalidated input. Here is an example how to access header variables:
// note the stripped HTTP_ prefix and the UPPERCASE characters: $rd->get('headers','USER_AGENT'); // AgaviRequestDataHolder class $rd->getHeader('User-Agent'); // AgaviWebRequestDataHolder classYou validate them by using the
source
parameter in your validator specification:
<validator ... source="headers"> <argument>USER_AGENT</argument> </validator> <-- or perhaps for referer checking: --> <validator ... source="headers"> <-- you could e.g. check for a valid (and/or allowed) URL, the non-existance of Javascript/HTML/SQL/XPath/whatever code snippets etc. --> <argument>REFERER</argument> </validator>
string
validator as people could send you Javascript, SQL
or whatever malicious input data not only using parameters, but instead trying (sometimes delayed) attacks using cookies
and headers. The next time you use plain SQL and someone surfs on your page using a referer set to
' OR 1=1 -- "|# you will definitively be thankful for strict validation and _good_
validation rules in your validators.
Please note, that you can access $_SERVER
(like $_SERVER['SERVER_NAME']
) and $_ENV
directly,
as it is mostly not user provided data (and not really standardized as well).
IS_AJAX
header parameter to your action if the
current request was issued via Javascript's XmlHttpRequest
method, see the following snippets:
<!-- validate.xml --> <validator class="SearchHttpXRequestedWithValidator" name="no_ajax_submit" required="false" provides="search[submit_search]" source="headers"> <arguments> <argument>X_REQUESTED_WITH</argument> </arguments> <ae:parameters> <ae:parameter name="export">IS_AJAX</ae:parameter> </ae:parameters> </validator>and
/* SearchHttpXRequestedWithValidator.class.php */ class SearchHttpXRequestedWithValidator extends AgaviStringValidator { protected function validate() { $argument = $this->getArgument(); $value = $this->getData($argument); $this->export(false !== stristr($value, 'XMLHttpRequest')); return true; } protected function checkAllArgumentsSet($throwError = true) { return true; // see AgaviValidator::checkAllArgumentsSet(); used to export default value in case of missing parameter } }Just add the custom
SearchHttpXRequestedWithValidator
to an autoload.xml
, use it in a validation XML file and ask
for AJAX requests in your action's execute*()
method (or wherever) like this:
$this->is_ajax_request = $rd->getHeader('IS_AJAX', false);↑
$this->getResponse()->setCookie('cookieName', $value); $this->getResponse()->setCookie('cookieName', $value, '+14 days'); // notice the hot strtotime syntaxIf you want to read cookies, you have to validate them as they are user provided input data. Other cookie related methods can be found in AgaviWebResponse. To remove a cookie just call
unsetCookie($name, $path, $domain, $secure, $httponly)
with the same
parameters you used for setting the cookie. To get a cookie value simply call getCookie($name)
.
↑public function executeHtml(AgaviRequestDataHolder $rd) { // ... some code ... if (some_constraint) { // redirect to a specific URL: $this->getContainer()->getResponse()->setRedirect($absolute_target_url); // or redirect like this ('relative'=>false may be omitted as Agavi sets it automatically for you) $this->getResponse()->setRedirect($this->getContext()->getRouting()->gen('route.to.somewhere', $params_array, array('relative' => false))); return; // no false or AgaviView::NONE here } // or forward to another action (the default 404 page in this case): return $this->createForwardContainer(AgaviConfig::get('actions.error_404_module'), AgaviConfig::get('actions.error_404_action')); }When forwarding, the output type et cetera should be respected. If you want to set these things explicitly, use the three optional parameters
$arguments
, $outputType
and $requestMethod
.
$this->getResponse()->setHttpStatusCode('301'); // 301 moved permanently $this->getResponse()->setHttpStatusCode('304'); // 304 not modified $this->getResponse()->setHttpStatusCode('402'); // 402 payment required ;-) $this->getResponse()->setHttpStatusCode('404'); // 404 not found $this->getResponse()->setHttpStatusCode('405'); // 405 method not allowed $this->getResponse()->setHttpStatusCode('500'); // 500 internal server error // etc. pp., you probably know all those HTTP codes...↑
Agavi
. You can override the PHP defaults
by adding parameters to the <storage>
element in app/config/factories.xml
.
<storage class="AgaviSessionStorage"> <ae:parameter name="session_save_path">some path</ae:parameter> <!-- current directory used for session data storage --> <ae:parameter name="session_cookie_lifetime">10800</ae:parameter> <!-- lifetime of the session cookie in seconds --> <ae:parameter name="session_cookie_path">/</ae:parameter> <!-- path on the domain (single slash for all paths) --> <ae:parameter name="session_cookie_domain">www.example.com</ae:parameter> <!-- cookie domain (be careful with leading '.' as cookies are then visible to all subdomains) --> <ae:parameter name="session_cookie_secure">true</ae:parameter> <!-- true for secure connection cookie only --> <ae:parameter name="session_cookie_httponly">true</ae:parameter> <!-- set httponly flag (beware of XMLHttpRequest issues - otherwise it's not a bad measure (even though it doesn't protect you e. g. from HTTP TRACE attacks or the like) --> <!-- for other parameters see AgaviSessionStorage file --> </storage>↑
app/config/output_types.xml
(to include them in the
master template) or create/delete slot containers dynamically "on the fly" in your views.
Defining slots inside <layer>
definitions in output_types.xml
could look like this:
<slots> <slot name="header" module="Default" action="Header" /> <slot name="navigation" module="Default" action="Navigation.Main" /> <ae:parameter name="page">%core.template_dir%/static/page.html</ae:parameter> </slot> <slot name="footer" module="Default" action="Footer"/> </slots>To be able to use the given parameters in a slot you have to validate them. If you want to create slot containers in a view, try it like this:
// in view: $this->getLayer('content')->setSlot('slotName', $this->createSlotContainer( 'ModuleName', // name of module to use 'Some.AwesomeActionName', // name of action to execute array('param' => 'value'), // parameters to pass to the slot 'html', // output type to use 'write' // request method to use )); // in template: if (!empty($slots['slotName']) { echo $slots['slotName']; }You don't have to pass parameters explicitly to the slot, as it will have a copy of all the current request parameters. The output type and request method parameters are optional, too. To give you an impression on how flexible Agavi's output type, layer and slot handling is, see this:
// to change the action of a specific slot (you can set the module name as well) $this->getLayer('content')->getSlot('slotName')->setActionName('SomeNewActionName'); // create and append layer when you need one in a view $this->appendLayer($this->createLayer('AgaviFileTemplateLayer', 'content'));
app/config/output_types.xml
and
then choose which layout to load in your views like this:
$this->setupHtml($rd, 'layoutName');Imagine you have some modules and each module has multiple (nested) actions. You then can easily override
setupHtml
in each module's BaseView
class and set a default
layout name for that module (so different sections of your homepage could be organized into
modules with different layouts/designs all over).
<!-- app/config/output_types.xml example part: --> <layout name="default"> <layers> <layer name="content"/> <layer name="decorator"> <slots> <slot name="header" module="Default" action="Header" /> <slot name="navigation" module="Default" action="Navigation" /> <slot name="footer" module="Default" action="Footer"/> </slots> <ae:parameters> <parameter name="template">%core.template_dir%/decorators/DefaultMaster</ae:parameter> </ae:parameters> </layer> </layers> </layout> <layout name="another"> <layers> <!-- xinclude layer definitions from default layout (should use better xpath expression and 1.0 style namespaces...) --> <xi:include xpointer="xmlns(agavi=http://agavi.org/agavi/1.0/config) xpointer(//agavi:output_type[@name='html']/agavi:layouts/agavi:layout[@name='default']/agavi:layers/*)" /> </layers> </layout>↑
$ dev/bin/agavi action-wizard Module name: Posts Action name: Add Space-separated list of views to create for Add [Success]: Input Success ErrorYour code (pseudo-style) could then look something like this:
// in the input handling action (e.g. Default_AddFoobarAction): public function executeWrite(AgaviRequestDataHolder $rd) { // ... some code ... update via models etc. if ($errors_while_trying_to_do_something_useful) { return 'Error'; } return 'Success'; } public function handleError() // could be handleWriteError or similar { // error handling like (p)re-populating the form return 'Error'; } public function getDefaultViewName() { // by default show the input template return 'Input'; } // in the input and success views: public function executeHtml(AgaviRequestDataHolder $rd) { // set some attributes etc. for your template } // in the error view public function executeHtml(AgaviRequestDataHolder $rd) { // re-use the input template for presenting the errors in the form $this->getLayer('content')->setTemplate('Input'); }By re-using the input template in your error view you can avoid duplicating code without sacrificing later maintainability. You shouldn't go for having less and less files in these cases, as you'll notice later on, that you need to add views and templates when you add other output types. Without proper structuring you would need to write a lot of unmaintainable spaghetti code.
decorator
in this case)
in the view:
$this->getContext()->getLayer('decorator')->setTemplate(AgaviConfig::get('core.templates').'/some_other_master');How to change layouts using
output_types.xml
is described under
How to load different layouts?.
↑AgaviFactoryConfigHandler
source file. The shutdown
sequence is in reverse. So controller is shut down first, then request, routing, user, translation manager and storage.$_GET etc.
are unset in AgaviRequest::startup()
and/or
AgaviWebRequest::startup()
, but with the above startup sequence you are able to register
$_GET etc.
as a routing source in init
. To disable certain functionalities of
Agavi just look at settings.xml
:
<setting name="use_database">true</setting> <setting name="use_logging">true</setting> <setting name="use_security">false</setting> <setting name="use_translation">true</setting>To change the implementing classes have a look at the
factories.xml
file. Please be careful, when you
enable or disable features in settings, as you won't be able to access certain features. For example the
AgaviDateTimeValidator
needs the translation manager to work correctly.if (AgaviConfig::get('core.use_logging', false)) { // do your logging... }↑
public function initialize(AgaviContext $context, array $parameters = array()) { $this->context = $context; }
Every time you initialize a model a $context parameter is passed, along with an array of custom params. This is the way to do it:
$this->getContext()->getModel('Model_Name', 'Module_Name', array('foo','bar'));
Models are loaded in the way shown above, and additionally, you can skip the second parameter ONLY if the model is not inside any module, but located at the main application's models/ folder (at the lowest level). Bare in mind not to use the word 'Model' when passing the first parameter. It's 'FooBar', not 'FooBarModel'.
Here's a few extra things you need to know about Agavi models:
settings.xml
file or include via PHP, YAML,
INI files or whatever other formats you like. Agavi gives you a very convenient way to use custom
configs and have them validated and cached for further requests like normal Agavi config files.
Just start by defining a new config handler in your app's config_handlers.xml
file:
<handler pattern="%core.config_dir%/project/foo/*.xml" class="AgaviReturnArrayConfigHandler" />This line tells Agavi to use the built-in (yet deprecated)
AgaviReturnArrayConfigHandler
class
for all XML files to be found in the app/config/project/foo/
directory.$config = include AgaviConfigCache::checkConfig(AgaviConfig::get('path_to_one_of_the_xml_configfiles'));The above defined handler parses the XML file and returns an associated array of all defined tags and values for easy access. You may use the config like this:
if (!isset($config['some'])) { throw new LogicException('element "some" missing!'); } foreach ($config['some']['bar'] as $foo => $data) { $this->doSomethingWithEach($foo, $data); }The XML file for the above pseudo code could be like that:
<?xml version="1.0" encoding="UTF-8"?> <some> <baz>asdf</baz> <foo>-1</foo> <bar> <some>...more data and xml...</some> <more>...more data and xml...</more> <deep>...more data and xml...</deep> <structs>...more data and xml...</structs> </bar> </some>To be able to write code without all the validational
isset()
etc. statements
you may validate your XML structure against a well defined XML schema definition (XSD) and
use your own custom config handlers. Create a schema file, define and create your
config handler class and add it to autoload.xml
:
class FooDefinitionConfigHandler extends AgaviXmlConfigHandler { // have a look at AgaviXmlConfigHandler and AgaviIXmlConfigHandler // and then // implement necessary methods and validate your XML structure... }
<handler pattern="%core.config_dir%/project/foo/*.foo.xml" class="FooDefinitionConfigHandler"> <validation>%core.config_dir%/xsd/foo_definition.xsd</validation/> </handler>For examples of XSDs and custom config handler classes just have a look at the Agavi source code. In case you don't want to use Agavi's XML parser, you'll need to resolve parents, filter content and order <configuration> blocks by contexts and environments all by yourself (see
AgaviXmlConfigParser
).
For further information and if you wonder what's possible, please head to the API docs or ask the devs.
To quote the configuration section of the Agavi 1.0
RELEASE_NOTES file extensively:
Agavi now supports any number of XML Schema (including XML Schema Instance declarations), RELAX NG or Schematron files to be validated against config files or merged result documents in several different stages, can apply XSL transformations (including those declared in PIs) and has extended namespaces support that allow backwards-compatibility to old config files in future versions. New-style configuration handlers extending AgaviXmlConfigHandler are now given a single AgaviXmlConfigDomDocument (extends DOMDocument) instance to work on, which already holds only the relevant <configuration> blocks in the correct order. Together with other subclasses of ext/dom classes, it provides several convenience methods to ease the processing of configuration files, but of course, all capabilities of DOM, including XPath, are usable inside configuration handlers.
StaticFilesConfigHandler
.
clone
and put the generator
files
into your dev/libs/propel
folder while the runtime
files may be put into your project's app/lib/vendor/propel
directory. Use this distinction when you
don't need the generator files in production environments.dev/libs/propel/bin/propel-gen dev/db
. Generate
your runtime config files into the dev/db
directory via convert-conf
command. Your classes will probably be in your dev/db/build
directory. The important files in your dev/db
folder
are schema.xml
, runtime-conf.xml
and build.properties
. Configure your directories similar to this:
#relative to propel-gen script propel.output.dir = ../../.. propel.php.dir = ${propel.output.dir}/app/lib/vendor/propel propel.phpconf.dir = ${propel.output.dir}/dev/db/config propel.sql.dir = ${propel.output.dir}/dev/db/sqlConfigure your include path to include the Propel runtime and OM classes.
$path = '/your/project/libs' . PATH_SEPARATOR . '/your/project/app/lib/vendor/propel'; set_include_path(get_include_path() . PATH_SEPARATOR . $path);You may setup your Propel connection in your
app/lib/config/database.xml
file similar to this:
<database name="propelom" class="AgaviPropelDatabase"> <ae:parameter name="config">%core.app_dir%/config/your-project-conf.php</ae:parameter> </database>You may generate the classmap and conf files to the wanted directory or just copy
classmap-your-project-conf.php
and
your-project-conf.php
(from Propel's dev/db/build/conf
) to Agavi's app/config
directory. You
can of course define custom database connection overrides when needed.
use_database
to
true
in your app/config/settings.xml
.
targets
parameter on a layer in output_types.xml
. This enables your application to
have different templates and let Agavi choose the first one that matches the given targets. The following
snippet gives an example on how one could implement mobile templates that reuse existing actions and views by
letting Agavi first look for Success.mobile.php
before trying to use Success.php
.
That way you can have a mobile version of your website where it is not strictly necessary to duplicate all templates
for the mobile version as you can reuse the original templates. In case you want to adjust a template for mobile
devices you can just put a *.mobile.php
file next to the existing normal one. The {extension}
placeholder is usually set via the default_extension
parameter of the renderer used for the layer.
<layer name="content"> <parameters> <parameter name="targets"> <parameter>${directory}/${template}.mobile${extension}</parameter> <parameter>${directory}/${template}${extension}</parameter> </parameter> </parameters> </layer>↑
output_types.xml
or set dynamically
within views:
<layer name="user_generated_content" renderer="twig" />The above feature enables you to define specific layouts with different layers where e.g. all layers are rendered via a
AgaviPhpRenderer
while the layer that displays sensitive user provided content is rendered using a template
engine renderer of your choice that has features such as sandboxing or limited sets of capabilities. The given example
uses a renderer named twig
(see AgaviTwigRenderer
and the Twig homepage for more information) that
is usually defined for specific output types. With this knowledge you could do crazy things like XML or Javascript islands
within HTML all by defining according layers with appropriate custom renderers that handle escaping or whatever.<output_type name="html"> <renderers default="proxy"> <renderer name="proxy" class="AgaviProxyRenderer"> <ae:parameter name="renderers"> <ae:parameter>twig</ae:parameter> <ae:parameter>pphp</ae:parameter> </ae:parameter> </renderer> <renderer name="pphp" class="ProjectPhpRenderer"> <ae:parameter name="assigns"> <!-- default assigns of Agavi omitted --> </ae:parameter> <ae:parameter name="default_extension">.php</ae:parameter> <ae:parameter name="var_name">t</ae:parameter> </renderer> <renderer name="twig" class="AgaviTwigRenderer"> <ae:parameter name="assigns"> <-- default assigns of Agavi omitted --> </ae:parameter> <ae:parameter name="default_extension">.twig</ae:parameter> <ae:parameter name="extract_vars">true</ae:parameter> <ae:parameter name="options"> <ae:parameter name="auto_escape">true</ae:parameter> <ae:parameter name="auto_reload">true</ae:parameter> </ae:parameter> </renderer> </renderers> <layouts default="standard"> <!-- standard layout with a content and a decorator layer --> <layout name="standard"> <layer name="content" /> <layer name="decorator"> <ae:parameter name="directory">%core.template_dir%/Html</ae:parameter> <ae:parameter name="template">HtmlMaster</ae:parameter> </layer> </layout> </layouts> </output_type>You can find the AgaviProxyRenderer source code on Github. It was written and tested with Agavi v1.0.7. If you use the given example the normal Agavi behaviour on template lookup patterns is still working. That way, if you use the
targets
parameter for layers given in
a previous answer,
Agavi would try to lookup templates in this order:
*.mobile.twig
*.twig
*.mobile.php
*.php
AgaviTwigRenderer
. It sets automatic escaping of all extracted variables (set via setAttribute()
in views and enables automatic reloading of modified templates even when the core.debug
setting of Agavi is set to false.
By default Agavi uses the Twig template caching mechanism when not run in debug mode. For details see the renderer and the Twig docs.
*.de_DE.php
or
en/*.php
were omitted for brevity reasons in the above order listing.
<setting name="agavi.action.path">%core.module_dir%/${moduleName}/impl/${actionName}/Action.class.php</setting> <setting name="agavi.cache.path">%core.module_dir%/${moduleName}/impl/${actionName}/cache.xml</setting> <setting name="agavi.template.directory">%core.module_dir%/${module}/impl/</setting> <setting name="agavi.validate.path">%core.module_dir%/${moduleName}/impl/${actionName}/validate.xml</setting> <setting name="agavi.view.path">%core.module_dir%/${moduleName}/impl/${viewName}View.class.php</setting> <setting name="agavi.view.name">${actionName}/${viewName}</setting>You can also put this into its own
<settings>
block and xi:include
from elsewhere.
Using the above example settings you get the following directory structure:
app // application directory modules // contains all modules Default // module name config // contains module specific config files (e.g. module.xml, autoload.xml, config_handlers.xml, validators.xml) impl // implementation directory with all actions (and their related files) lib // should contain module specific base classes for views, actions, models or custom validators etc. models // module specific model classesAll actions, views, templates and cache/validation configs are inside the impl directory in the folder next to each action. You can see fast, if a validation or caching file exists for that action and what views and templates are available for it.
Default_Sub_Marine_SuccessView
instead
of Default_Sub_MarineSuccessView
(in directory Default/Sub
are files like
MarineAction.class.php
and MarineSuccessView.class.php
).
config_handlers.xml
:
<handlers> <handler pattern="%core.module_dir%/*/impl/*/validate.xml" class="AgaviValidatorConfigHandler"> <validation>%core.agavi_dir%/config/xsd/validators.xsd</validation> </handler> <handler pattern="%core.module_dir%/*/impl/*/cache.xml" class="AgaviCachingConfigHandler"> <validation>%core.agavi_dir%/config/xsd/caching.xsd</validation> </handler> <handlers>
app/config/routing.xml
file. There are multiple nice things to learn about how to define and nest routes and
what additional parameters might prove to be helpful.<!-- matches "/de" or "/en" at the beginning of the URL and uses either value to set the locale, with British Pounds Sterling as the currency. Execution will not stop if this route matches. If it matches, the matched fragment will be stripped from the input URL for the following routes. This route has the "imply" flag, which means that it will be included in generated routes even if the gen() call does not explicitly mention it. The matched "language" parameter will not be available as a request parameter in the code, because it's "ignore"d --> <route pattern="^/({locale:[a-z]{2}(_[A-Z]{2})?})" stop="false" imply="true" cut="true" locale="${locale}@currency=GBP"> <callbacks> <callback class="AgaviSampleAppLanguageRoutingCallback" /> </callbacks> <ignores> <ignore>locale</ignore> </ignores> </route> <!-- If the HTTP Accept: header contains "application/json" (i.e. if you do an XMLHTTPRequest with one of the usual JS frameworks), set the output type to "json". Look at output_types.xml for the declaration of that output type. Execution will not stop if this route matches. This is nice for making XMLHTTPRequest calls. --> <route pattern="application/json" source="_SERVER[HTTP_ACCEPT]" output_type="json" stop="false" /> <route pattern="^/auth" module="%actions.login_module%"> <routes> <route name="login" pattern="^/login$" action="%actions.login_action%" /> <!-- A nested route. This will match the URL "/login/logout" - matched fragments from the parent route are stripped from the URL string for child routs. --> <route name="logout" pattern="^/logout$" action="Logout" /> </routes> </route>This example is taken from the sample application's routing.xml. The
pattern
parameters of the login and logout routes have a trailing $
.
Without them URLs like /auth/login/foo/bar
could match later on, which is
most probably not your desired behaviour.name
routes, to be able to generate
them in templates or views with $url = $ro->gen('route.name', $params, $options);
. In case
a nested route's name starts with a dot (".name") you have to specify route.name
for
route generation. If your nested route is just a normal name you can use it for gen()
calls directly.source
, but _SERVER
values etc. as well). In the above example URLs that
are requested via XHR (using one of the common Javascript libraries out there, that set the correct accept headers
for such calls) would use the executeJson()
method in the matched action's view (instead of the probably
default executeHtml()
you normally use in your web application).
↑page
parameter with default value start
:
<route name="page.products" pattern="^/(page:[A-Za-z0-9_\-]+)/products" module="Products"> <defaults> <default for="page">start</default> </defaults> <route name=".detail" pattern="^/({id:\d+})$" action="Products" /> </route>Optional
page
parameter with default value start
:
<route name="page.products" pattern="^(/{page:[[:alnum:]]+})?/products" module="Products"> <defaults> <default for="page">/{start}</default> </defaults> <route name=".detail" pattern="^/({id:\d+})$" action="Products" /> </route>↑
<route name="hash" pattern="/(hash:[a-fA-F\d]\{32\})$"> <!-- or try it like this: --> <route name="hash" pattern="/({hash:[a-fA-F\d]{32}})$">↑
pattern
attribute you can use
the known regular expression pattern modifiers.
As Agavi depends on PCRE
you can just use the /i
modifier to make matching case insensitive (or any other modifier to try other things):
<route name="asdf_and_foo_case_insensitive" pattern="... ((?i)asdf|foo) ..."> <!-- to only make a part of the pattern case insensitive just use braces: --> <route name="asdf_case_insensitive" pattern="... (((?i)asdf)|foo) ...">If you want the complete matching to be case insensitive, use
(?i)
at the beginning of the pattern. Please be aware of duplicate content issues when using
case-insensitive URLs or optional trailing slashes etc. Read your favorite SEO-related
articles/blogs to get some more information about this.
↑$ro->gen()
.
See the following examples:
// just as a reminder, a complete URL may consist of these parts: // scheme://[authority[:password]@](host_name|\[host_address\])[:port][/hierarchical/path/to/resource[?search_string][#fragment_id]] // default call for route generation: $ro->gen('some.sub.route', $routeParams, $optionsArray); // default routing generation options from AgaviWebRouting: $this->defaultGenOptions = array_merge($this->defaultGenOptions, array( // separator, typically & for HTML, & otherwise 'separator' => '&', // whether or not to append the SID if necessary 'use_trans_sid' => false, // scheme, or true to include, or false to block 'scheme' => null, // authority, or true to include, or false to block 'authority' => null, // host, or true to include, or false to block 'host' => null, // port, or true to include, or false to block 'port' => null, // fragment identifier (#foo) 'fragment' => null, )); // there are other options like: // tell Agavi to generate absolute (or relative) URLs: $ro->gen('some.sub.route', $routeParams, array('relative' => false)); // tell Agavi to use the current URL with parameters (e.g. re-submit a form to the same URL): $ro->gen(null);You may combine the available options as you like. ↑
// get array with all matched routes of the current request $route_names_array = $this->getContext()->getRequest()->getAttribute('matched_routes', 'org.agavi.routing'); // you'll get an array that looks something like this with as many routes as matched: array('product.edit' ...); // which you can use to get an information array from it: $route = $this->getContext()->getRouting()->getRoute($route_name_from_array); // and determine the executed module and action from that: $module = $route['opt']['module']; $action = $route['opt']['action'];The above example with some further explanations can be found in DracoBlue's blog entry about matched routes. In case you wonder, why multiple routes may have matched, think about changing the output type using a route, cutting locale parameters from routes or setting up things via routing callbacks (see the first routing related FAQ entry for examples). ↑
<route pattern="^/({locale:[a-z]{2}})" stop="false" imply="true" cut="true"> <callbacks> <callback class="LanguageRoutingCallback" /> </callbacks> <ignores> <ignore>locale</ignore> </ignores> </route>
There are three events you could use for all purposes: onMatched, onNotMatched, onGenerate. To create a new routing callback extend the class AgaviRoutingCallback and make use of those. The most exceptional thing to realize about them is that parameters, options, and user parameters are passed by reference, so any update in them will affect the route execution or generation. This is perfect since you could use it to your advantage.
// e.g. in view/template when linking to resources $ro->gen('music.genre+rss', ...); // link to rss feed of genres $ro->gen('music.genre', ...); // link to HTML listing of genresThe first route generates a link to your normale URL with the added extension
".rss"
as you specified in your routing.xml
file. You
may add various other output types to your setup and will be able to support
multiple types of data depending on the called route. To use this, follow the
next steps.
output_types.xml
file:
<output_type name="rss"> <renderers default="php"> <renderer name="php" class="AgaviPhpRenderer"> </renderer> </renderers> <parameters> <parameter name="http_headers"> <parameter name="Content-Type">application/rss+xml; charset=UTF-8</parameter> </parameter> </parameters> </output_type>You then define the routes you need to use and generate feeds in
routing.xml
:
<route name="rss" pattern=".rss$" stop="false" cut="true" output_type="rss" /> ... <route name="music" pattern="^/music"> <route name=".genre" pattern="^/genre/(name:[\S\s]+)$"> </route> </route>The
cut="true"
attribute tells Agavi to cut ".rss" from the route and continue
with matching the routes as stop="false"
.execute*()
methods for each
output type to generate your music genre listing (in this example):
public function executeRss(AgaviRequestDataHolder $rd) { // generate your feed using whatever you like (SimpleXML, XMLWriter, some external library ...) return $xml_output_for_feed; }You may add the appropriate execute methods as fallback to your base views to prevent Agavi exceptions when somebody calls an URL with
".rss"
that has no specified
executeRss()
method (and forward/redirect to a 404 page or the HTML
output or whatever fits your needs).<route name="output_json" pattern="JSON" source="_SERVER[HTTP_X_REQUEST]" output_type="json" stop="false" /> <!-- or --> <route name="output_ajax" pattern="XMLHttpRequest" source="_SERVER[HTTP_X_REQUESTED_WITH]" output_type="ajax" stop="false" />Use whatever you think is appropriate - the different Javascript libraries or frameworks usually set the correct request headers for you to use in pattern matching for your routes. So using this scheme a single URL may return HTML or JSON depending on the request headers sent. Awesome! :-) ↑
factories.xml
(if you want to relax
things a bit, which is not advised):
<validation_manager class="AgaviValidationManager"> <ae:parameter name="mode">strict|conditional|relaxed</ae:parameter> </validation_manager>The "strict" mode is the default and recommended mode as it is the most secure one. It requires you to validate ALL inputs. This includes not only form inputs, but also route parameters, headers, files and cookies.
AgaviAndoperatorValidator
or AgaviXoroperatorValidator
in a nested szenario? ;)
For a simple start try some validator chaining like this:
<validators method="write"> <validator class="and" name="email_rules"> <validator class="string" name="email_too_short" trim="true"> ... </validator> <validator class="email" name="email_valid"> ... </validator> ...some more if you like... </validator> </validators>With the
AgaviAndoperatorValidator
you can bundle multiple validators (and hence their errors)
into one. If the first validator fails, the execution stops there (= 1 error message)
but all validators are still required to pass validation.
Veikko provides another example, that is probably helpful in certain cases:
<validator class="or"> <validators> <validator class="email" required="false" > <arguments> <argument>email</argument> </arguments> </validator> <validator class="MyPhoneNumberValidator" required="false" > <arguments> <argument>phone</argument> </arguments> </validator> </validators> <errors> <error>Please provide email or phone</error> </errors> </validator>The
MyPhoneNumberValidator
is a custom validator. With Agavi you
can write your own custom validators to perform certain tasks on your input data
before action execution. See How
to create a custom Validator? to learn, how easy it is.
↑<validator ... severity="critical">The base class
AgaviValidator
defines multiple error severity constants
in case of failure. They are SUCCESS
, INFO
, SILENT
, NOTICE
,
ERROR
and CRITICAL
. For further details just have a look
at the AgaviValidator
source code. The severity levels work like this:
CRITICAL
aborts validation instantly and other validators will not be executed.ERROR
is the default that marks the argument as not valid. The argument
will be in the validation report and thus may be populated via the FormPopulationFilter (FPF).
The value will of course not be available for your actions etc.SUCCESS
is used when the validator succeeded.INFO
means that the failing of the validator does not have an impact on the result of
the whole validation process. The 'failed' value will still be available for actions.SILENT
means, that there will be no error in the validation report
(and thus no automatic repopulation in html forms via the FormPopulationFilter can happen).NOTICE
is similar to SILENT
, but will generate an error for the validation
report. The validation process in general does not fail, but the value will not be available.<validator ... provides="password_valid"> <validator ... depends="password_valid">Veikko provides another nice example for this:
<validator ... required="false" provides="street_set"> <arguments base="contact"> <argument>Street</argument> </arguments> </validator> <validator ... required="true" depends="contact[street_set]"> <arguments base="contact"> <argument>Zip</argument> </arguments> </validator>
↑Note: argument’s
base
attribute affects also provides/depends attributes (see example above)!
<validator class="string" provides="password_required"> <argument>password</argument> <error>Password is required</error> </validator> <validator class="string" provides="confirm_required"> <argument>password_confirmation</argument> <error>Password confirmation is required</error> </validator> <validator class="equals" depends="password_required confirm_required"> <argument>password_confirmation</argument> <ae:parameter name="value">password</ae:parameter> <ae:parameter name="asparam">true</ae:parameter> <error>Password confirmation doesn't match Password</error> </validator>In this simple case the password match validator is only run if both previous string validators succeed. ↑
register[Write|Read|etc]Validators()
methods:
$validators = array(); $arguments = array('date'); $errors = array('' => 'something went wrong'); $parameters = array('export' => 'somedate', 'required' => true); $validator = new SomeSpecialDateValidator(); $validator->initialize($this->getContext(), $parameters, $arguments, $errors); $validators[] = $validator; $this->getContainer()->getValidationManager()->registerValidators($validators);For convenience you may use the
createValidator()
method to create validators like this (pseudocode; not tested):
$validationManager->createValidator('Some_CaptchaValidator', array(0 => 'captcha_comment'), array('' => 'You entered a wrong validation code.'), array('type' => 'integer', 'severity' => 'error', 'method' => null, 'required' => true, 'translation_domain' => 'default.captcha', 'name' => 'invalid_captcha', 'domain' => $captcha_domain ) );↑
AgaviImageFileValidator
?<validator class="AgaviImageFileValidator" severity="error" required="false"> <arguments> <argument>image</argument> </arguments> <errors> <error>Provided image invalid.</error> <error for="max_width">Please only use images with a maximum width of 500px.</error> <error for="min_width">Your provided image should have a minimum width of at least 10px.</error> <error for="max_size">Maximum allowed filesize is 50kb.</error> <error for="format">Only JPEG, GIF and PNG images will be accepted.</error> </errors> <ae:parameters> <ae:parameter name="max_width">500</ae:parameter> <ae:parameter name="min_width">10</ae:parameter> <ae:parameter name="max_size">51200</ae:parameter> <ae:parameter name="format"> <ae:parameter>jpeg</ae:parameter> <ae:parameter>gif</ae:parameter> <ae:parameter>png</ae:parameter> </ae:parameter> </ae:parameters> </validator> <validator class="AgaviFileValidator" severity="error" required="false"> <arguments> <argument>css</argument> </arguments> <errors> <error>Invalid file. Use non-empty files with a '.css' extension.</error> </errors> <ae:parameters> <ae:parameter name="min_size">0</ae:parameter> <ae:parameter name="extension">css</ae:parameter> </ae:parameters> </validator>Please make sure, that you have proper security constraints when working with user provided content/files. There are many possible attack vectors and you should NEVER trust any uploaded content or your determined MIME types or extensions. Guessing the file types is not sufficient on its own as there could be other content hidden inside. Just google GIFARs (e.g. see Billy (BK) Rios' blog), JPGARs or other MIME type and file upload security related topics for a start. For enhanced security you could perhaps deliver uploaded content using a second web server instance, which does only have read only access for very specific directories (where you move uploaded files to) and will not execute scripts. ↑
handleError()
method to provide a value if the parameter is not present in the request.public function handleError(AgaviRequestDataHolder $rd) { if (!$rd->hasParameter('readonly_or_disabled_field')) { $this->context->getRequest()->setAttribute('populate', new AgaviParameterHolder($rd->getParameters() + array('readonly_or_disabled_field' => 'default value')), 'org.agavi.filter.FormPopulationFilter'); } return 'Input'; }This will work most of the time, but you will notice missing values in some cases as
$rd
does not contain all global parameters to repopulate. If you notice this try to repopulate everything
using true
and then set your value manually.// repopulate your form in the ErrorView (with the already posted data): $this->getContext()->getRequest()->setAttribute('populate', true, 'org.agavi.filter.FormPopulationFilter'); // pre-fill form with some values: $data = array("name_of_input_field" => "some_value"); $this->getContext()->getRequest()->setAttribute('populate', $data, 'org.agavi.filter.FormPopulationFilter'); // or pre-fill it like this: $data = new AgaviRequestDataHolder(); $data->setParameter('name_of_input_field', 'some_value'); $this->getContext()->getRequest()->setAttribute('populate', $data, 'org.agavi.filter.FormPopulationFilter');↑
FormPopulationFilter
to repopulate form fields and set necessary CSS error classes etc.
↑isSet
or isNotEmpty
to validate route parameters?AgaviIssetValidator
and
AgaviIsNotEmptyValidator
, as you have to validate all parameters in strict mode.
The difference of "set" vs. "empty" is as follows:AgaviWebRequestDataHolder
parameters are considered empty even when they're an empty string,
because browsers submit an empty string for empty form fields. (Otherwise required="false"
on
validators for optional fields would require a somewhat cumbersome implementation)
↑inArray
validator?<validator class="inarray" name="invalid_day" required="false"> <arguments> <argument>day</argument> </arguments> <errors> <error for="type">Only some values are allowed as a day.</error> </errors> <ae:parameters> <ae:parameter name="type">string</ae:parameter> <ae:parameter name="case">true</ae:parameter> <ae:parameter name="values"> <ae:parameter>monday</ae:parameter> <ae:parameter>tuesday</ae:parameter> <ae:parameter>wednesday</ae:parameter> <ae:parameter>thursday</ae:parameter> <ae:parameter>friday</ae:parameter> <ae:parameter>saturday</ae:parameter> <ae:parameter>sunday</ae:parameter> </ae:parameter> </ae:parameters> </validator>If you don't like the
values
parameter array specification, you may use a
custom separator
and put all values in one line:
<validator class="inarray" name="invalid_day" required="false"> <arguments> <argument>day</argument> </arguments> <errors> <error for="type">Only some values are allowed as a day.</error> </errors> <ae:parameters> <ae:parameter name="type">string</ae:parameter> <ae:parameter name="case">true</ae:parameter> <ae:parameter name="values">monday,tuesday,wednesday,thursday,friday,saturday,sunday</ae:parameter> <ae:parameter name="sep">,</ae:parameter> </ae:parameters> </validator>The
'case'
parameter is false by default and specifies if the comparison
should be case-sensitive or not (true
= case-sensitive comparison).
↑AgaviInarrayValidator
from the
answer above to validate a form with multiple submit buttons. Each button element has the same
name
attribute day
and a value
attribute set to monday
,
tuesday
etc. Everything works as expected, your daynames are matching a route with a
pattern like ^/(day:(monday|tuesday|etc...))$
and your validator just works. At least
until you check your multiple submit button scenario in Internet Exploder. The IE displays your
nicely CSS formatted buttons (like <button type="submit" name="day" value="sunday">some HTML</button>
),
but does submit the inner content instead of the value attribute values (as any other browser
out there sticking to standards does). So what now? Javascript not allowed, one form per button not possible, other
alternatives fading fast...Easy. Breathe, just change your validators a bit to
validate array input and use the names of the submit buttons to get your information to the server:
<button type="submit" name="day[sunday]" value="whatever">some HTML</button> <validator class="string" name="invalid_day" required="false"> <arguments base="day[]"> <argument></argument> </arguments> </validator>Other examples for the validation of arrays are like this (thanks to Veikko once again :P):
<input name="rows[id]" ...> <arguments base="rows"> <argument>id</argument> </arguments> <!-- or --> <arguments> <argument>rows[id]</argument> </arguments>or:
<input name="rows[1][id]" ...> <arguments base="rows[]"> <argument>id</argument> </arguments>or:
<input name="rows[]" ...> <arguments base="rows[]"> <argument/> </arguments>Note the usage of the
base
attribute in certain places. In addition to this there's
a new AgaviArraylengthValidator
since Agavi v1.0, which lets you specify min
and max
parameters
to validate the number of elements in an array.
Furthermore, if you would like to overwrite an input array with results from a validator, you can
do so by specifying an export parameter as follows:
<arguments base="rows[]"> <argument/> </arguments> <ae:parameters> <ae:parameter name="export">rows[%2$s]</ae:parameter> </ae:parameters>↑
AgaviDateTimeValidator
?AgaviDateTimeValidator
is a versatile tool. It can not only validate several
date and time formats, but may also convert those values to a format that suits you in your actions/views
(like unix timestamp, a formatted date etc.). The following is a small example, on how to
cast your input date into another format:
<validator method="write" class="datetime" required="true" name="invalid_birthday" translation_domain="registration.errors"> <arguments> <argument>birthday</argument> </arguments> <ae:parameters> <ae:parameter name="formats">dd.MM.yyyy</ae:parameter> <ae:parameter name="export">casted_birthday</ae:parameter> <ae:parameter name="cast_to"> <ae:parameters> <ae:parameter name="type">date</ae:parameter> <ae:parameter name="format">yyyy-MM-dd</ae:parameter> </ae:parameters> </ae:parameter> </ae:parameters> <errors> <error>Invalid date format.</error> </errors> </validator>Notice the
cast_to
parameter and the parameters given to it. You can find further
explanations and other possible parameters in the AgaviDateTimeValidator
class. Another thing to notice is the translation_domain
attribute, which allows
for easy translation of validation error messages.<validator class="AgaviDateTimeValidator"> <arguments> <argument name="AgaviDateDefinitions::DATE">day</argument> <argument name="AgaviDateDefinitions::MONTH">month</argument> <argument name="AgaviDateDefinitions::YEAR">year</argument> </arguments> <errors> <error>Validation error</error> </errors> <ae:parameters> <ae:parameter name="export">MyDatestamp</ae:parameter> </ae:parameters> </validator>The
AgaviDateTimeValidator
supports the strtotime
syntax
(see ticket #1018).
Therefore it's possible to do something like this:
<!-- beware of +1 month as imho it's always 30 days... --> <ae:parameter name="min">+1 week</ae:parameter> <ae:parameter name="max">-18 years</ae:parameter> <!-- according to the ticket you can even use timezones: --> <ae:parameter name="min">last monday this month 08:00UTC</ae:parameter>See the custom AgeValidator of DracoBlue which could have used the default date validator (but chose not to because of error messages).
use_translation
set to true
in settings.xml
for this validator and its translations to work.
by()
method calls you can get information about errors and incidents of your validated arguments.
Ticket #1022
contains examples:
$report = $this->getContainer()->getValidationManager()->getReport(); // or: $report = $this->getContainer()->getValidationManager()->getReport()->createQuery(); // check for a valid name $nameValid = !$report->byArgument('name')->has(); // every by() method returns a clone of the query object, so you don't have to create new instances $nameMissing = $report->byArgument('name')->byErrorName('required')->has(); // check for optional image file $myoptionalfileIsImage = $report->byArgument(new AgaviValidationArgument('myoptionalfile', AgaviWebRequestDataHolder::SOURCE_FILES)) ->byValidator('is_image') ->getResult() === AgaviValidator::SUCCESS;The "old" way (but still functioning, I'd guess) would be to use named validators, checking the incidents of the validators in question and then dealing with them accordingly:
public function handleError(AgaviRequestDataHolder $rd) { $vm = $this->getContainer()->getValidationManager(); if (count($vm->getReport()->getValidatorResult('my_special_validator')->getIncidents())) { return 'CriticalError'; } else { return 'Error'; } }Above example taken from http://blog.veikko.fi/. ↑
AgaviEqualsValidator
with the default value parameter
of the Agavi(*)Request::getParameter()
method:
<validator class="equals" required="false"> <argument>user_disabled</argument> <ae:parameter name="value">1</ae:parameter> </validator>and then in action/view:
$rd->getParameter('user_disabled', 0); // default value = 0 when checkbox is not submittedPresuming you don't want to rely on the default behaviour of checkbox input fields, there is a slightly different solution that makes use of a combination of validators to always have a checkbox value after validation (by setting the missing value to the default):
<validator class="or" required="true"> <validator class="number" required="false"> <argument>user_disabled</argument> </validator> <validator class="set" required="false"> <ae:parameter name="export">user_disabled</ae:parameter> <ae:parameter name="value">0</ae:parameter> </validator> </validator>With this validator your checkbox field will always be defined and transmitted and all internal methods will work as expected on 'normal' input fields. Which solution you choose depends once again on your preferences and if it is useful to not have an unchecked checkbox input field submitted (so
$rq->hasParameter()
method behaves differently).
↑parent="..."
attribute. The value
allows you to specify a parent validation file to include. Agavi needs this to let
you use those shortnames for built-in validators instead of fully qualified class names.
<!-- in specific action's validator reference the module's validator --> parent="%core.module_dir%/Programs/config/validators.xml" <!-- in the module's validator file reference the application specific validator file --> parent="%core.config_dir%/validators.xml"The application's validator file then references Agavi's default validation file as parent. Proceeding from this it's probably clear, that you can define validators in those various places to prevent repeating yourself.
<?xml version="1.0" encoding="UTF-8" ?> <ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" xmlns="http://agavi.org/agavi/config/parts/caching/1.0" xmlns:xi="http://www.w3.org/2001/XInclude"> <ae:configuration> <cachings> <caching lifetime="30 seconds" enabled="true"> <groups> <group>some_group_name_to_use_for_this_action</group> <group source="request_data">some_parameter</group> <group source="request_attribute">or_attribute</group> </groups> <views> <view>Success</view> </views> <!-- include default output type 'html' caching (template variables and request attributes necessary to cache javascript and styles correctly and show a complete page etc. --> <xi:include href="%core.config_dir%/cache_default.xml" xpointer="xmlns(ae=http://agavi.org/agavi/config/global/envelope/1.0) xmlns(ac=http://agavi.org/agavi/config/parts/caching/1.0) xpointer(/ae:configurations/ae:configuration/ac:cachings/ac:caching/*)" /> </caching> </cachings> </ae:configuration> </ae:configurations>The included snippet resides in
app/config/cache_default.xml
(or whatever fits your requirements)
and could look like this (depends on your application, your used slots, global request attributes they need
to function when being fragment cached etc.):
<?xml version="1.0" encoding="UTF-8" ?> <ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" xmlns="http://agavi.org/agavi/config/parts/caching/1.0"> <ae:configuration> <!-- this file contains default settings to be used via xincludes in caching xml files throughout the application - be careful with changes to this file as multiple cache.xml files are affected --> <cachings> <caching> <output_types> <output_type name="html"> <layer name="content" /> <template_variable>_content_type</template_variable> <template_variable>_robots</template_variable> <template_variable>_meta_tags</template_variable> <template_variable>_title</template_variable> <template_variable>theme</template_variable> <request_attributes> <request_attribute namespace="app.layout">javascript_files</request_attribute> <request_attribute namespace="app.layout">stylesheets</request_attribute> <request_attribute namespace="www.example.com">some_pageid</request_attribute> </request_attributes> </output_type> </output_types> </caching> </cachings> </ae:configuration> </ae:configurations>Request attributes are set in your cached views and defined here explicitly to have them available, when fragment cached versions of your actions/views are displayed. Without this, the attributes wouldn't be available. Praise the XInclude for centralized storage, management, retrieval and reuse of snippets. ↑
AgaviValidator
or
one of its siblings and add a validate()
method to it. Using parameters you can
customize the behaviour of your validator. In case validation fails you simple throw errors
and these error names may then be used for (translated) error messages in the validation XML file.
class SampleCustomValidator extends AgaviValidator { protected function validate() { // if(!parent::validate()) // in case you extend another validator // { // return false; // } // get input data (e.g. from input field) $inputdata = $this->getData($this->getArgument()); // when required="false" is set on the validator // we usually can't set a default value for the following // actions - thus we override checkAllArgumentsSet() to return // true and return our default value in case of no input data if (empty($inputdata)) { $this->export(array()); } if (!$this->hasParameter('something')) { $this->throwError(); return false; } if ($some_constraint !== $this->getParameter('some_param', $default_value)) { $this->throwError('one_problem'); if ($even_more_problems_arise) { $this->throwError('two_problems'); } return false; } // check for consistency, ask your Agavi models for help, do whatever // you like to validate your input data - e.g. like this: $finder = $this->getContext()->getModel('BlogPostFinder'); $post = $finder->findById($inputdata); if (null === $post) { $this->throwError('not_found'); return false; } // export your validated data (e.g. the $post object so your actions don't need to retrieve it again) // you may optionally specify a name here as 2nd parameter (or set one using the ae:parameter 'export') $this->export($post); return true; } // you only need to override this method, when you want to export default values // to your actions in case no input was given (that is, when 'required' is set to 'false') protected function checkAllArgumentsSet($throwError = true) { return true; // look at AgaviValidator::checkAllArgumentsSet() } }After creating your own custom validator you have to add the class to
autoload.xml
and may start using it in your validation definitions like this:
<validator class="SampleCustomValidator" name="valid_post" required="false"> <arguments> <argument>foo</argument> </arguments> <errors> <error>Strange things happened.</error> <error for="not_found">Blog post not found. Specify valid ID.</error> <error for="one_problem">"Some people, when confronted with a problem, think >I know, I'll use regular expressions.<.</error> <error for="two_problems">Now they have two problems" -- Jamie Zawinski.</error> </errors> <ae:parameters> <ae:parameter name="something">true</ae:parameter> <ae:parameter name="some_param">value</ae:parameter> <ae:parameter name="export">bar</ae:parameter> </ae:parameters> </validator>Everything should be rather obvious when reading this code. The
export
parameter is optional
and specifies the name under which to export your data to the request (so foo
will
be available to your actions as bar
).AgaviValidator::checkAllArgumentsSet()
when you want to export default values to your actions when required="false"
.
Keep in mind, that required="false"
just prevents the validator
from running when the argument the validator checks is not available.
parent
validators until you find agavi/src/config/defaults/validators.xml
. Within
the chain you may always define short names everywhere. Examples are validator definitions specific to a module
or sets that are used project wide. You may create validator definitions and set sensible default parameters
for your own custom validators. Just add the following in a [validators].xml
file:
<validator_definition name="sample" class="SampleCustomValidator"> <ae:parameter name="something">true</ae:parameter> </validator_definition>The best about this mechanism isn't that you save on typing some classnames in your validator definitions. The convenient thing is, that you may not only reuse validators (built-in or custom), but also reuse existing validators with different sets of pre-defined parameters. Another advantage is, that you may easily change your validator implementation later on without changing the lot of action validators you may have already in use in your project. Just a cheap example to give you an example:
<validator_definition name="valid_username" class="AgaviStringValidator"> <!-- ... etc. ... --> <ae:parameter name="min">5</ae:parameter> </validator_definition>Later on you extend that example by using the RegExp validator to check for valid characters and patterns:
<validator_definition name="valid_username" class="AgaviRegexValidator"> <!-- ... etc. ... --> <ae:parameter name="pattern">[a-zA-Z_\-]{5,25}</ae:parameter> </validator_definition>As your project grows you now want to check for the availability of usernames upon registration and export valid
User
objects instead of just strings (unique names) to your actions. You create your own
custom validator for this, do all your checks on the given arguments and export the User
object to
the request:
<validator_definition name="valid_username" class="CustomUsernameValidator"> <!-- ... etc. ... --> <ae:parameter name="check_availability">false</ae:parameter> </validator_definition> <validator_definition name="valid_available_username" class="CustomUsernameValidator"> <ae:parameter name="check_availability">true</ae:parameter> </validator_definition>The above example reuses the
CustomUsernameValidator
with different parameter settings.
To use your defined validators simply write validators for your actions and use the specified shortnames as class
name like this:
<validator class="valid_available_username" name="some_valid_and_available_username" required="false"> <arguments> <argument>username</argument> </arguments> <errors> <error>Username neither valid nor available.</error> </errors> </validator>As you can see, the system is easily extendable and you're able to easily change certain validations in one place instead of various files in all your different actions or modules. The maintainance is eased and error prone changes of parameters everywhere in your project are no longer one of your concerns as you may simply add another shortname or change settings for the existant ones. ↑
app/[ModuleName]/cache/[ActionName].xml
files)
individually per environment. Just edit the factories.xml
file and set the
enable_caching
parameter on the AgaviExecutionFilter
accordingly:
<ae:configuration context="web" environment="development.*"> <execution_filter class="AgaviExecutionFilter"> <ae:parameters> <ae:parameter name="enable_caching">false</ae:parameter> </ae:parameters> </execution_filter> </ae:configuration>↑
cache
directory (or one of its subdirectories in case you have a deeper folder structure for actions/slots)
of your module and set some groups as keys to differentiate. Keys can be taken from different
sources (request parameters, current locale, etc.). The groups are used to build a directory
structure (folder names are base64 encoded) used for caching (see app/cache/content/
directory)
fragments of your output.<?xml version="1.0" encoding="UTF-8"?> <ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" xmlns="http://agavi.org/agavi/config/parts/caching/1.0"> <ae:configuration> <cachings> <caching lifetime="30 seconds" enabled="true"> <groups> <group>show_article_detail</group> <group source="user_attribute" namespace="tld.example.sub">userId</group> <group source="request_data">item[id]</group> <group source="global_request_data">article_id</group> <group source="global_request_data" namespace="cookies">someLocaleCookie</group> <group source="callback">getCustomCachingKeyFromActionMethod</group> <group source="locale" /> </groups> <views> <view>Success</view> </views> <output_types> <output_type name="html"> <layer name="content" /> <template_variable>_content_type</template_variable> <template_variable>_robots</template_variable> <template_variable>_meta_tags</template_variable> <template_variable>_title</template_variable> <request_attributes> <request_attribute namespace="app.layout">javascript_files</request_attribute> <request_attribute namespace="app.layout">stylesheets</request_attribute> </request_attributes> </output_type> </output_types> </caching> </cachings> </ae:configuration> <ae:configuration environment="production"> <cachings> <caching method="read" lifetime="24 hours" enabled="true"> <group source="callback">tryAnotherCustomCachingKeyFromActionMethod</group> </caching> </cachings> </ae:configuration> <ae:configuration environment="staging"> <cachings> <caching method="read write" lifetime="30 seconds" enabled="false"> <groups> <group>show_article_detail</group> <group source="user_attribute" namespace="tld.example.sub">userId</group> <group source="request_data">someObjectInRequest</group> </groups> </caching> </cachings> </ae:configuration> </ae:configurations>As you see, you can use different sources for your caching keys (groups) and define different settings for different environments. It is also possible to create custom (more complex) group keys by using a caching callback method of your action, which returns the caching key to use. In case the action method throws an
AgaviUncacheableException
nothing is cached.
↑source
attribute values are available and what do they do?source
attribute in the
AgaviExecutionFilter
.
'0'
if string value is null
, false
or empty
.AgaviUncacheableException
nothing will be cached.AgaviConfig::get()
to let you use configuration settings. Use
something like core.debug
or modules.modulname.Some
as element content.AgaviExecutionContainer
(not its request!) to be used as a group.namespace
attribute is specified, the default is AgaviRequestDataHolder::SOURCE_PARAMETERS
,
which maps to normal request parameters. You can specify headers
,
files
and cookies
as namespace
attribute values to use other sources as group names.AgaviTranslationManager
and uses it as a group name.namespace
attribute to get attributes from the
request, that are stored using a namespace.source="global_request_data"
without a namespace
attribute.global_request_data
, but gets its parameters from
the current execution container's request instead of the global request. A namespace
attribute may be specified as well.namespace
attribute to use for the attribute value lookup.isAuthenticated()
method's return value if the current context's user is
an instance of AgaviISecurityUser
.hasCredentials()
method's return value if the current context's user is
an instance of AgaviISecurityUser
.object
Agavi
first tries to use the object's __toString()
method or secondly looks,
if a spl_object_hash
method exists to get a nice key for the group name.
↑app/cache/content
) and may overwrite
parts of each other which may result in wrongly generated routes/texts etc. (first user hitting a
page in one environment would prime the cache and the user of the other environment would see
this cached file instead of his environment's version). To prevent those problems you could
use the environment's name from Agavi's settings as a caching group in your caching definition files:<group source="configuration_directive">core.environment</group>In case you have many default groups you could define a default cache group file or sandbox and XInclude everything you need. See the validation question about DRY for some example that enables you to quickstart.
is_slot
parameter
into account and use the same action as a slot and a full page. As a workaround use is_slot
somewhere in your caching groups or callbacks.17 03 * * * find /srv/www/dir/of/your/app/cache/content -xdev -type f -mmin +1800 -print0 | xargs -r0 rm -v > /dev/null
app/config/translation.xml
and using the AgaviTranslationManager
in your code:
<?xml version="1.0" encoding="UTF-8"?> <ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" xmlns="http://agavi.org/agavi/config/parts/translation/1.0"> <ae:configuration> <available_locales default_locale="en_US@currency=GBP"> <!-- German, Germany --> <available_locale identifier="de_DE"> <ae:parameter name="description">Deutsch</ae:parameter> </available_locale> <!-- English, United States --> <available_locale identifier="en_US"> <ae:parameter name="description">English</ae:parameter> </available_locale> <!-- Chinese Simplified, China (alias for zh_Hans_CN, Simplified Han) --> <available_locale identifier="zh_CN"> <ae:parameter name="description">简体中文</ae:parameter> </available_locale> <!-- Chinese Traditional, Taiwan (alias for zh_Hant_TW, Traditional Han) --> <available_locale identifier="zh_TW"> <ae:parameter name="description">繁體中文</ae:parameter> </available_locale> </available_locales> <translators default_domain="default"> <translator domain="default"> <translator domain="errors"> <message_translator class="AgaviSimpleTranslator"> <ae:parameter name="Login"> <ae:parameter name="de"> <ae:parameter name="Wrong Password">Falsches Passwort</ae:parameter> <ae:parameter name="Wrong Username">Ungültiger Benutzername</ae:parameter> <ae:parameter name="Please supply a password.">Bitte geben Sie ein Kennwort ein.</ae:parameter> <ae:parameter name="Username's too short (5 chars min).">Benutzername zu kurz (fünf Zeichen Minimum).</ae:parameter> </ae:parameter> </ae:parameter> </message_translator> </translator> <message_translator class="AgaviGettextTranslator"> <ae:parameter name="text_domains"> <ae:parameter name="menu">%core.app_dir%/data/i18n</ae:parameter> <ae:parameter name="layout">%core.app_dir%/data/i18n</ae:parameter> <ae:parameter name="Login">%core.app_dir%/data/i18n</ae:parameter> <ae:parameter name="SearchEngineSpam">%core.app_dir%/data/i18n</ae:parameter> <ae:parameter name="ErrorActions">%core.app_dir%/data/i18n</ae:parameter> </ae:parameter> </message_translator> <date_formatter> <ae:parameter name="type">date</ae:parameter> <ae:parameter name="format">full</ae:parameter> </date_formatter> </translator> <translator domain="long_date"> <date_formatter> <parameters> <ae:parameter name="type">datetime</ae:parameter> <ae:parameter name="format">EEEE, dd.MM.yyyy - HH:mm</ae:parameter> </ae:parameters> </date_formatter> </translator> <translator domain="medium"> <date_formatter> <ae:parameters> <ae:parameter name="type">datetime</ae:parameter> <ae:parameter name="format">dd.MM.yyyy</ae:parameter> </ae:parameters> </date_formatter> </translator> </translators> </ae:configuration> <ae:configuration environment="development.*"> <translators default_domain="default"> <translator domain="default"> <message_translator class="AgaviGettextTranslator"> <ae:parameter name="store_calls">%core.cache_dir%/data</ae:parameter> <!-- other params are inherited from generic config --> </message_translator> </translator> </translators> </ae:configuration> </ae:configurations>As you can see, you can use the
AgaviSimpleTranslator
or the AgaviGettextTranslator
(or roll out your own database-backed translator). For simple applications gettext or the simple mode
will probably suffice long enough. If you want to translate validation error messages you have to
specify the translation_domain
parameter like in the datetime validator example.
// display date value in dd.MM.yyyy format (see translators defined above) echo $tm->_d($field['value'], 'medium'); // display value of ...parameter name="Wrong Password"... element according to current locale in specified domain echo $tm->_('Wrong Password', 'default.errors.Login'); // display value in a different currency (using british pounds for this call instead of default for current locale) echo $tm->_c(1200, null, '@currency=GBP'); // display time based on specific timezone instead of current locale echo $tm->_d(time(), null, '@timezone=Europe/London');↑
translations.xml
file:
<currency_formatter> <ae:parameter name="format"> <ae:parameter name="en">¤#,##0.00</ae:parameter> <ae:parameter name="de">#.##0,00 ¤</ae:parameter> </ae:parameter> </currency_formatter>The currency formatting follows the same rules as the number formatting. The only exception is the special symbol "¤". This symbol is the Unicode Character 'CURRENCY SIGN' (U+00A4) and is used as a placeholder for the currency symbol. To use a different currency than the one currently set on the active locale use:
$tm->_c(123.45, 'some.domain', '@currency=GBP');You can globally change the default currency for a locale in your
translations.xml
.
↑app/config/action_filters.xml
and app/config/global_filters.xml
respectively. The wrapping of filters allows you, to let each filter alter the request and response.
The AgaviExecutionTimeFilter
can be used as followed in action_filters.xml
:
<?xml version="1.0" encoding="UTF-8"?> <ae:configurations xmlns="http://agavi.org/agavi/config/parts/filters/1.0" xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"> <!-- only for web contexts in development envs --> <ae:configuration environment="development.*" context="web"> <filters> <filter name="ExecutionTimeFilter" class="AgaviExecutionTimeFilter"> <!-- insert a comment with the total runtime of that container in the response's source code (adds the total page runtime and one comment for each slot) --> <ae:parameter name="comment">true</ae:parameter> <!-- only run for HTML to let JSON etc. unaltered --> <ae:parameter name="output_types"> <ae:parameter>html</ae:parameter> </ae:parameter> </filter> </filters> </ae:configuration> </ae:configurations>Agavi uses the filter mechanism internally e.g. for action execution (see
AgaviExecutionFilter
) or forcing
credential constraints for secure actions (see AgaviSecurityFilter
).
If action filters are not enough for you, you may define filters, that are run globally.
A global_filters.xml
example file could therefore look like this:
<?xml version="1.0" encoding="UTF-8"?> <ae:configurations xmlns="http://agavi.org/agavi/config/parts/filters/1.0" xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"> <!-- this section is only for web contexts --> <ae:configuration context="web"> <filters> <!-- TidyFilter tries to clean up the HTML code for you --> <filter name="TidyFilter" class="AgaviTidyFilter"> <!-- only run on HTML instead of JSON and other output types you may have defined --> <ae:parameter name="output_types"> <ae:parameter>html</ae:parameter> </ae:parameter> <!-- don't let errors stop it --> <ae:parameter name="ignore_errors">true</ae:parameter> <!-- set some options to the filter --> <ae:parameter name="tidy_options"> <ae:parameter name="output-xhtml">true</ae:parameter> <ae:parameter name="numeric-entities">true</ae:parameter> <ae:parameter name="clean">true</ae:parameter> <ae:parameter name="indent">true</ae:parameter> </ae:parameter> <!-- default encoding to use on any web apps is utf-8 of course --> <ae:parameter name="tidy_encoding">utf8</ae:parameter> </filter> <!-- FormPopulationFilter tries to automagically pre/repopulate form fields and insert specific and general error messages from validators etc. in your page at positions (and with markup) you find suitable for your application --> <filter name="FormPopulationFilter" class="AgaviFormPopulationFilter"> <ae:parameters> <!-- only run for request method "write" (=POST on web) by default (can be changed at runtime, of course) --> <!-- if you omit this, it will never run --> <ae:parameter name="methods"> <ae:parameters> <ae:parameter>write</ae:parameter> </ae:parameters> </ae:parameter> <!-- only run for output type "html" (so it doesn't break on, say, JSON data) --> <!-- if you omit this, it will run for all output types --> <ae:parameter name="output_types"> <ae:parameters> <ae:parameter>html</ae:parameter> </ae:parameters> </ae:parameter> <!-- error message insertion rules --> <!-- they are run in sequence; once the first one matched, execution stops --> <!-- errors that belong to more than one field (e.g. date validator) can be handled using "multi_field_error_messages" "normal" errors are handled through "field_error_messages" errors that yield no match and those that have no corresponding field are inserted using rules defined in "error_messages". --> <!-- for all field error messages. --> <ae:parameter name="field_error_messages"> <!-- ${htmlnsPrefix} is either empty (for HTML) or something like "html:" for XHTML documents with xmlns="..." notation. Always use this, makes your code more bullet proof. XPath needs the namespaces when the document is namespaced --> <!-- all input fields that are not checkboxes or radios, and all textareas --> <ae:parameter name="self::${htmlnsPrefix}input[not(@type='checkbox' or @type='radio')] | self::${htmlnsPrefix}textarea"> <!-- if this rule matched, then the node found by the rule is our starting point for inserting the error message(s). --> <!-- can be any of "before", "after" or "child" (to insert as prev, next sibling or last child) --> <ae:parameter name="location">after</ae:parameter> <!-- a container groups all errors for one element. ${errorMessages} is a string containing all errors (see below) --> <ae:parameter name="container"><![CDATA[<div class="errors">${errorMessages}</div>]]></ae:parameter> <!-- this defines the HTML for each individual error message; those are then put into the container. ${errorMessage} is the error message string --> <ae:parameter name="markup"><![CDATA[<p class="error">${errorMessage}</p>]]></ae:parameter> </ae:parameter> <!-- all other inputs - note how we select the parent element and insert ourselves as last child of it --> <ae:parameter name="parent::*"> <ae:parameter name="location">child</ae:parameter> <ae:parameter name="container"><![CDATA[<div class="errors">${errorMessages}</div>]]></ae:parameter> <ae:parameter name="markup"><![CDATA[<p class="error">${errorMessage}</p>]]></ae:parameter> </ae:parameter> </ae:parameter> <!-- <parameter name="multi_field_error_messages"> </parameter> --> <!-- everything that did not match any of the rules above, or errors that do not belong to a field --> <ae:parameter name="error_messages"> <!-- insert before the element --> <!-- that can be an input, or a form, if the error does not belong to a field or didn't match anywhere else --> <ae:parameter name="self::*"> <ae:parameter name="location">before</ae:parameter> <!-- no container here! we just insert paragraph elements --> <ae:parameter name="markup"><![CDATA[<p class="error">${errorMessage}</p>]]></ae:parameter> </ae:parameter> </ae:parameter> </ae:parameters> </filter> </filters> </ae:configuration> <-- disable the tidy filter for all development* environments to force devs to think about valid code ;) --> <ae:configuration environment="development.*"> <filters> <filter name="TidyFilter" enabled="false" /> </filters> </ae:configuration> <-- try to relax things a bit for all production related environments --> <ae:configuration environment="production.*"> <filters> <filter name="FormPopulationFilter"> <ae:parameter name="ignore_parse_errors">true</ae:parameter> </filter> </filters> </ae:configuration> </ae:configurations>It first uses the
AgaviTidyFilter
to cleanup code
that may contain some markup glitches (think CMS or
user-generated content). After that the AgaviFormPopulationFilter
runs,
repopulates values to your various form fields (inputs, textareas and the like) and inserts
several types of error messages into your document. You may use XPath expressions as you like
to insert input field specific or common messages with whatever container markup you prefer.
For development environments the tidy filter is disabled and for production environments the
form population filter is set to ignore parse errors.
↑global_filters.xml
or
action_filters.xml
files, the markup order is essential in
certain situations. The execution order of filters should be:
TOP --> BOTTOM --> DISPATCHFILTER --> BOTTOM --> TOPThis means, that dependant on the things your filters are doing in the chain BEFORE or AFTER the request dispatching you may have to move them up or down in the XML file to get your ideal execution order. ↑
sample/test
directory as a
start and after copying stuff to your own project try editing /test/config/suites.xml
to have your own test suites and tests. Modify run-tests.php
and
add some required files to the autoloader for libraries you need.
<?xml version="1.0" encoding="UTF-8"?> <ae:configurations xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0" xmlns="http://agavi.org/agavi/config/parts/testing/suites/1.0"> <ae:configuration> <suites> <suite name="Default"> <testfiles> <testfile>OneNiceUnitTest.php</testfile> <testfile>AndSomeNiceFlowTest.php</testfile> </testfiles> </suite> <suite name="Login"> <testfiles> <testfile>AnotherNiceUnitTest.php</testfile> <testfile>AndAnotherNiceFlowTest.php</testfile> </testfiles> </suite> </suites> </ae:configuration> </ae:configurations>After creating test cases and suites you may run them like this (or whatever, just have a look at the available commandline parameters):
php -c php.ini run-tests.php "TextUI/Command.php" --verbose↑
$favoriteIDE
?// save the following as autocomplete.php somewhere in your project structure (or use it via project includes), // it should give you code completion in Agavi templates for assigns (defined in app/config/output_types.xml): <?php exit(); $slots = array(); $template = array(); // agavi 0.11 default $t = array(); // agavi 1.0 default $tm = new AgaviTranslationManager(); $ro = new AgaviWebRouting(); $rq = new AgaviWebRequest(); $ct = new AgaviController(); $us = new AgaviSecurityUser(); $rd = new AgaviWebRequestDataHolder(); ?>In models, views and actions you better use some nice PHP documentation to give your IDE as much information about parameters and return values as you can. Further on you may use type hinting like this:
/* @var $author AuthorModel */ $author = $this->getContext()->getModel('Author');If that's still not good enough for you, you can try to further enhance your project's base view like this:
class ProjectBaseView extends AgaviView { /** * @var AgaviWebRequest */ protected $rq = null; /** * @var AgaviWebRouting */ protected $ro = null; /** * @var AgaviController */ protected $ct = null; /** * @var AgaviWebResponse */ protected $rs = null; /** * @var AgaviSecurityUser */ protected $us = null; /** * @var AgaviTranslationManager */ protected $tm = null; /** * @var AgaviValidationManager */ protected $vm = null; /** * @var AgaviDatabaseManager */ protected $dbm = null; /** * @var AgaviContext */ protected $ctx = null; /** * @var bool */ protected $isSlot = false; /** * @see AgaviView::initialize() */ public function initialize(AgaviExecutionContainer $container) { parent::initialize($container); $this->ctx = $this->getContext(); $this->dbm = $this->ctx->getDatabaseManager(); $this->rq = $this->ctx->getRequest(); $this->ro = $this->ctx->getRouting(); $this->ct = $this->ctx->getController(); $this->us = $this->ctx->getUser(); $this->tm = $this->ctx->getTranslationManager(); $this->vm = $this->ctx->getValidationManager(); $this->rs = $this->getResponse(); $this->isSlot = $container->hasParameter('is_slot'); return true; } }Not that you NEED this (or should use it), but some people may like these shortcuts. Have fun. :) ↑
$this->getResponse()->setExitCode(1);With this your Agavi PHP shell/console applications can now return correct status codes to your shell depending on success/error. :-) ↑