I try to migrate from ZF2 to ZF3 but many viewHelpers and validators do not work. But only the ones who overwrite ZendFrameworks viewhelpers / validators do not work...
I want f.e. to overwrite the CSRF validator to allow a higher timeout by default.
I have the following application configuration:
$config = array(
// This should be an array of module namespaces used in the application.
'modules' => array(
'Zend\Cache',
'Zend\Db',
'Zend\Log',
'Zend\Mail',
'Zend\Mvc\Console',
'Zend\Mvc\I18n',
'Zend\I18n',
'Zend\Mvc\Plugin\FilePrg',
'Zend\Form',
'Zend\Hydrator',
'Zend\InputFilter',
'zend\Form',
'Zend\Filter',
'Zend\Mvc\Plugin\FlashMessenger',
'Zend\Mvc\Plugin\Identity',
'Zend\Mvc\Plugin\Prg',
'Zend\Navigation',
'Zend\Paginator',
'Zend\Serializer',
'Zend\ServiceManager\Di',
'Zend\Session',
'Zend\Router',
'Zend\Validator',
'DoctrineModule',
'DoctrineORMModule',
'TwbBundle',
'AssetManager',
#'Reliv\ElFinder',
'ZfcUser', //https://github.com/ZF-Commons/ZfcUser
'ZfcUserDoctrineORM',
'BjyAuthorize', // https://github.com/bjyoungblood/BjyAuthorize
'Base',
'Product',
'Blog',
'Admin'
),
// These are various options for the listeners attached to the ModuleManager
'module_listener_options' => array(
// This should be an array of paths in which modules reside.
// If a string key is provided, the listener will consider that a module
// namespace, the value of that key the specific path to that module's
// Module class.
'module_paths' => array(
'./module',
'./vendor',
),
// An array of paths from which to glob configuration files after
// modules are loaded. These effectively override configuration
// provided by modules themselves. Paths may use GLOB_BRACE notation.
'config_glob_paths' => array(
'config/autoload/{,*.}{global,local}.php',
),
// Whether or not to enable a configuration cache.
// If enabled, the merged configuration will be cached and used in
// subsequent requests.
// 'config_cache_enabled' => true,
// The key used to create the configuration cache file name.
//'config_cache_key' => $stringKey,
// Whether or not to enable a module class map cache.
// If enabled, creates a module class map cache which will be used
// by in future requests, to reduce the autoloading process.
// 'module_map_cache_enabled' => true,
// The key used to create the class map cache file name.
#'module_map_cache_key' => $stringKey,
// The path in which to cache merged configuration.
'cache_dir' => "data/cache/",
// Whether or not to enable modules dependency checking.
// Enabled by default, prevents usage of modules that depend on other modules
// that weren't loaded.
// 'check_dependencies' => true,
),
// Used to create an own service manager. May contain one or more child arrays.
//'service_listener_options' => array(
// array(
// 'service_manager' => $stringServiceManagerName,
// 'config_key' => $stringConfigKey,
// 'interface' => $stringOptionalInterface,
// 'method' => $stringRequiredMethodName,
// ),
// )
// Initial configuration with which to seed the ServiceManager.
// Should be compatible with Zend\ServiceManager\Config.
// 'service_manager' => array(),
);
Module config of Base module:
namespace Base;
...
return [
...
'validators' => array(
'invokables' => [
\Zend\Validator\Csrf::class => Validator\Csrf::class
]
)
...
]
Base\Validator\Csrf:
<?php
namespace Base\Validator;
class Csrf extends \Zend\Validator\Csrf
{
protected $timeout = 1;
public function __construct($options = [])
{
parent::__construct($options);
die("THIS DOES NOT GETTING PRINTED! NOR DOES THE BREAKPOINT HIT.");
}
}
EDIT: Added autoload config
composer.json:
"autoload": {
"psr-4": {
"Base\\": "module/Base/src/"
}
}
EDIT 2: Possible bug in implementation of \Zend\Form\Element\Csrf ?
Interesting, the CsrfValidator just gets directly instantiated here...
/**
* Get CSRF validator
*
* #return CsrfValidator
*/
public function getCsrfValidator()
{
if (null === $this->csrfValidator) {
$csrfOptions = $this->getCsrfValidatorOptions();
$csrfOptions = array_merge($csrfOptions, ['name' => $this->getName()]);
$this->setCsrfValidator(new CsrfValidator($csrfOptions));
}
return $this->csrfValidator;
}
Stacktrace (breakpoint in \Zend\Validator\Csrf __construct())
StaticPage is another module of mine.
I also debugged with xdebug and set a break point in the CsrfFactory (return statement) to see, if it is used (but it isn't).
I thought I can overwrite services / validators etc. easily in ZF3... Did I miss something?
You can use a delegator to change the validator attached to the form element when the element is instantiated. Essentially a delegator allows you (in this case) to modify the form element after it has been constructed - the idea is explained well here.
In your case you would create a class:
<?php
namespace Base\Delegator;
use Zend\ServiceManager\ServiceLocatorInterface;
use Zend\ServiceManager\DelegatorFactoryInterface;
use Zend\Validator\Csrf as CsrfValidator;
class CsrfDelegatorFactory implements DelegatorFactoryInterface
{
public function createDelegatorWithName(ServiceLocatorInterface $serviceLocator, $name, $requestedName, $callback)
{
// construct the Csrf form element
$element = call_user_func($callback);
// set the validator with chosen timeout and other options
$element->setCsrfValidator(new CsrfValidator(
[
// ...
'timeout' => 10000
]
));
return $element;
}
}
Then map the delegator to Zend\Form\Element\Csrf in your application module.config.php:
'form_elements' => [
// ...
'delegators' => [
\Zend\Form\Element\Csrf::class => [
0 => \Base\Delegator\CsrfDelegatorFactory::class
],
]
],
Note that this is only changing the validator assigned by default to a Zend\Form\Element\Csrf and a csrf validator obtained by other means will not be affected.
Related
Really sorry if I've missed something here, but I have searched the issues and docs high and low, and I cannot find why this is not working. Perhaps I need another tutorial on using the internet ;)
Consider the following entity annotation, in the openapi_context section the body.description and responses.200.description have no effect at all, and its driving me slightly mad... You should know that I am using de/normalization contexts but that doesn't seem related so I have left it out.
/**
* User entity
*
* #API\ApiResource(
* collectionOperations={
* "authenticate"={
* "method"="POST",
* "status"=200,
* "path"="/authenticate",
* "controller"=UserLoginController::class,
* "openapi_context"={
* "responses"={"200"={"description"="API token and secret"}},
* "body"={"description"="Login details"},
* "summary"="User authentication",
* "description"="Provides auth tokens on success",
* },
* },
* }
* )
*
*/
class User implements UserInterface
{
...
}
The result (blue blocks as expected, red blocks not working):
I have spent way too much time on this issue as it is, I would really appreciate it if someone could help me put this to bed. I have checked/tried the following to no avail;
Creating Custom Operations and Controllers
Custom Symfony Action with API Platform bundle
[Question] Documentation API with Swagger #143
Changing Operations in the OpenAPI Documentation
Composer versions (relevant parts):
{
"require": {
"php": ">=7.2.5",
"api-platform/core": "^2.6",
"symfony/framework-bundle": "5.2.*"
}
}
After some experimenting, I found that the openapi_context annotation attribute does indeed seems to ignore the response documentation. It does however allow you to provide the request body description that you are missing:
#[ApiResource(
collectionOperations: [
'test' => [
'method' => 'POST',
'path' => '/test',
'openapi_context' => [
'summary' => 'The endpoint summary',
'description' => 'The endpoint description',
'requestBody' => [
'description' => 'The endpoint request body description', // This one
'content' => [
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/MyResource-some.group'
],
],
],
],
],
]
],
)]
I'm using PHP 8.0.3 and API Platform 2.6.3 while writing this, changing the annotations to docblocks should result in the same outcome for you though.
In order to document the endpoint response specification however, I had to implement a custom OpenApiFactoryInterface:
<?php declare(strict_types = 1);
namespace App;
use ApiPlatform\Core\OpenApi\Factory\OpenApiFactoryInterface;
use ApiPlatform\Core\OpenApi\Model\Operation;
use ApiPlatform\Core\OpenApi\Model\PathItem;
use ApiPlatform\Core\OpenApi\Model\RequestBody;
use ApiPlatform\Core\OpenApi\OpenApi;
use ArrayObject;
use UnexpectedValueException;
class MyResourceOpenApiFactory implements OpenApiFactoryInterface
{
private OpenApiFactoryInterface $openApiFactory;
public function __construct(OpenApiFactoryInterface $openApiFactory)
{
$this->openApiFactory = $openApiFactory;
}
public function __invoke(array $context = []): OpenApi
{
$openApi = ($this->openApiFactory)($context);
$components = $openApi->getComponents();
$schemas = $components->getSchemas();
if (null === $schemas) {
throw new UnexpectedValueException('Failed to obtain OpenApi schemas');
}
$pathItem = new PathItem(
'MyResource test endpoint',
'A test summary',
'A test description',
null,
null,
// Your custom post operation
new Operation(
'testMyResourceCollection', // the operation route name
[
'MyResource' // your resource name
],
[
// response specifications
'201' => [
'description' => 'test endpoint 201 response description',
'content' => [
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/MyResource-read', // your resource (read) schema
],
],
],
],
],
'A test endpoint summary',
'A test endpoint description',
null,
[],
new RequestBody(
'A test request body description',
new ArrayObject([
'application/json' => [
'schema' => [
'$ref' => '#/components/schemas/MyResource-write', // your resource (write) schema
],
],
]),
),
),
);
$paths = $openApi->getPaths();
$paths->addPath('/my_resources/test', $pathItem);
return $openApi;
}
}
Have this OpenApiFactoryInterface implementation wired by the service container as a decorator to api_platform.openapi.factory:
App\MyResourceOpenApiFactory:
decorates: 'api_platform.openapi.factory'
autoconfigure: false
Change the references to the example MyResource name to a resource name of your choice (like User).
Sidenote:
This whole process of customizing OpenApi endpoint documentation in API Platform is currently quite convoluted in my opinion. Use the implementation I provided as a reference to your own implementation, as you most likely need to make a few adjustments to it in order to make it satisfy your specific use case.
Is there any possibility to invalidate or delete PageCache for a particular action.
Consider this:
class SiteController extends Controller
{
public function behaviors()
{
return [
'pageCache' => [
'class' => PageCache::className(),
'duration' => Yii::$app->params['cacheTime'], // seconds
'variations' => [
Yii::$app->language,
Yii::$app->request->get('id'),
],
],
];
}
public function actionIndex( $id )
{
// action code
}
}
And now I want to remove/invalidate cache for
action en/site/index?id=1
Currently I am thinking to write some code in a console app but do not know how to achieve this.
EDIT1: I try to rebuild-invalidate cache manually for a specific action. The code can't relay on 'dependency' because it is almost impossible to implement for that action.
EDIT2: The task is to rebuild cache only for the specific action (page) leave other cache intact.
You can use TagDependency for more granular invalidation:
public function behaviors()
{
return [
'pageCache' => [
'class' => PageCache::className(),
'duration' => Yii::$app->params['cacheTime'], // seconds
'variations' => [
Yii::$app->language,
Yii::$app->request->get('id'),
],
'dependency' => new \yii\caching\TagDependency([
'tags' => [
Yii::$app->requestedRoute,
Yii::$app->request->get('id'),
],
]),
],
];
}
To invalidate cache:
TagDependency::invalidate(Yii::$app->cache, [
'site/index', // route of action
123, // ID of page
]);
If someone else needs ...
Yii2 does not provide a native function to invalidate the cache of a specific page, however there is the delete function of the cache component. It would however be necessary to know the generated key for the requested page but the function that generates this key is protected (calculateCacheKey ()). In this way, the best way would be to create your own class extending \yii\filters\PageCache.
'pageCache' => function () {
return new class extends \yii\filters\PageCache{
public function init(){
parent::init();
$this->except = ['index'];
$this->duration = Yii::$app->params['cacheTime'], // seconds;
$this->variations = [
Yii::$app->language,
Yii::$app->request->get('id'),
];
if(Yii::$app->request->get('IC') == 1)
Yii::$app->cache->delete($this->calculateCacheKey());
}
public function beforeCacheResponse(){
return Yii::$app->request->get('IC') != 1;
}
};
},
In the provided code, for simplicity, I am using an anonymous class (PHP 7).
Instead you can create your class as you wish and inform its path as the 'class' parameter, as seen in the configuration displayed in the question.
Note that I am using a simple logic to invalidate the cache, checking if there is a GET parameter IC == 1, you can use whatever logic you want.
If after invalidating the cache you do not want a new cache to be created, simply return false in beforeCacheResponse, it is from \yii\filters\PageCache.
You can invalidate the cache by using dependencies
'pageCache' => [
...
'dependency' => [
'class' => 'yii\caching\DbDependency',
'sql' => 'SELECT COUNT(*) FROM post',
],
http://www.yiiframework.com/doc-2.0/yii-filters-pagecache.html#$dependency-detail
If I understand correctly you are trying to disable caching only for a specific action and according to the DOCS you can use the following options to explicitly identify which action IDs to apply the cache filter OR which action IDs it should not.
$except array List of action IDs that this filter should not apply to. yii\base\ActionFilter
$only array List of action IDs that this filter should apply to.
The following should work for you
return [
'pageCache' => [
'class' => PageCache::className(),
'except'=>['index']
'duration' => Yii::$app->params['cacheTime'], // seconds
'variations' => [
Yii::$app->language,
Yii::$app->request->get('id'),
],
],
];
I have a Form having primary key on two fields (gid, bid). I need to add validation to block duplicate entries into database.
I have checked with ZF2 Solution for this . http://framework.zend.com/manual/2.2/en/modules/zend.validator.db.html#excluding-records . While this approach of handling composite keys is not look the ideal way, But still I am trying it because it look like only buil-in way. Now it require me to provide second field's value (value option in exclude), which is again a problem. As I am trying it
$inputFilter->add(array(
'name' => 'gid',
'required' => true,
'validators' => array(
array(
'name' => 'NotEmpty',
'options' => array(
'messages' => array(
'isEmpty' => 'required'
),
),
),
array (
'name' => 'Zend\Validator\Db\NoRecordExists',
'options' => array (
'table' => 'gtable',
'field' => 'gid',
'adapter' => $this->dbAdapter,
'messages' => array(
\Zend\Validator\Db\NoRecordExists::ERROR_RECORD_FOUND => 'The specified key already exists in database'
),
'exclude' => array(
'field' => 'bid',
'value' => [?],
),
)
),
)
));
How do I get this value, As Form is absolute separate Class/File than controller where I have the submitted form values. Is some better architecture solution of this problem exists Or Some hack to pass submitted field value to Form Class is only solution ?
Note : I am not in favor of Build My Validation Plugin for this task as short time is constraint for functionality.
You can do all the job in your form. To achieve that, you could define your forms as factories in your module Module.php.
Module.php
use MyNamespace\MyForm;
//NOTE THAT THE SERVICE MANAGER IS INJECTED. YOUR FORM COULD RECEIVE IT THROUGH THE CONSTRUCTOR
public function getServiceConfig()
{
return array(
'factories' => array(
'my_form' => function( $sm ) {
$form = new MyForm( $sm );
return $form;
},
),
);
}
When you want to use the form is as easy as use this code in your controller:
class MyController extends AbstractActionController
{
public function createAction() {
$form = $this->getServiceLocator()->get( 'my_form' ) );
(...)
}
}
And your MyForm.php
use Zend\Form\Form;
class MyForm extends Form
{
public $serviceManager, $request, $postData;
public function __construct( $serviceManager ) {
parent::__construct( null );
$this->serviceManager = $serviceManager;
$this->request = $serviceManager->get( 'Application')->getMvcEvent()->getRequest();
$this->postData = get_object_vars( $this->request->getPost() );
}
}
This way you can get advantage of the Service Manager within your form. And the public postData, where you'll find the bid value you're looking for to build your NoRecordExists filter.
You could add the parameters to the getInputFilter, like this :
getInputFilter($gid, $bid)
And then on the controller, when you set the filter you pass the 2 parameters, and then just check as $form->isValid(); ...
Alternative try this:
array(
'name' => 'Db\NoRecordExists',
'options' => array(
'table' => 'gtable',
'field' => 'gid',
'adapter' => $this->dbAdapter,
),
),
I'm unsure on your use case. If you were to add a database entry the primary keys for that table would not be known until you insert anyway - If you have foreign key constraints you could handle the exception from the database.
I am not in favor of Build My Validation Plugin for this task
The validator is also not designed to validate multiple fields as they are attached to a form element on a 1-1 basis. You will therefore need to create your own.
The below example has NOT been tested, so take it as an example of the approach rather than working code.
The key bit is the isValid method.
namespace MyModule\Validator\Db;
use Zend\Validator\Db\NoRecordExists;
class CompositeNoRecordExists extends NoRecordExists
{
protected $field2;
protected $field2Value;
public function __construct($options = null)
{
parent::__construct($options);
if (array_key_exists('field2', $options)) {
$this->setField2($options['field2']);
} else {
throw new \BadMethodCallException('Missing field2 option!');
}
}
protected function setField2Value(array $context)
{
if (! isset($context[$this->field2])) {
throw new \BadMethodCallException('Unable to find value for field 2');
}
$this->field2Value = $context[$this->field2];
}
public function isValid($value)
{
// The isValid() method is actually given a 2nd argument called $context
// Which is injected by the inputFilter, via the input and into the validator chain
// $context contains all of RAW form element values, keyed by thier element name.
// Unfortunately due to the ValidatorInterface you are unable to add this to the method
// signature. So you will need to be 'creative':
$args = func_get_args();
if (isset($args[1]) && is_array($args[1])) {
$this->setField2Value($args[1]);
} else {
throw new \BadMethodCallException('Missing validator context');
}
return parent::isValid($value);
}
public function getSelect()
{
$select = parent::getSelect();
$select->where->equalTo($this->field2, $this->field2Value);
return $select;
}
}
Then all you would need to do is update the validator config, adding the field2 field name.
array (
'name' => 'MyModule\Validator\Db\CompositeNoRecordExists',
'options' => array (
'table' => 'gtable',
'field' => 'gid',
'field2' => 'bid',
'adapter' => $this->dbAdapter,
'messages' => array(
\Zend\Validator\Db\NoRecordExists::ERROR_RECORD_FOUND => 'The specified key already exists in database'
),
)
),
I use made use the extend function to extend and adding custom rules on the Validation Class of Laravel 4.
Validator::extend('foo', function($attribute, $value, $parameters)
{
return $value == 'foo';
});
When I validate the rule using the newly created custom extension, it returns validation.foo if the rule fails. Is there a way to define a generic/ default message when extending the validation class in Laravel 4?
The Laravel 4 docs specifically state you need to define an error message for your custom rules.
You have two options;
Option 1:
$messages = array(
'foo' => 'The :attribute field is foo.',
);
$validator = Validator::make($input, $rules, $messages);
Option 2:
Specify your custom messages in a language file instead of passing them directly to the Validator. To do so, add your messages to custom array in the app/lang/xx/validation.php language file:
'custom' => array(
'foo' => array(
'required' => 'We need to know your foo!',
),
),
In case someone is wondering about Laravel 5: just add your message to validation.php right under all the default messages. For example:
<?php
return [
// .. lots of Laravel code omitted for brevity ...
"timezone" => "The :attribute must be a valid zone.",
/* your custom global validation messages for your custom validator follow below */
"date_not_in_future" => "Date :attribute may not be in future.",
where date_not_in_future is your custom function validateDateNotInFuture.
Laravel will pick the message each time you use your rule for any field and you won't have to use custom array unless you want to override your global message for specific fields.
Full code to implement the validator follows.
Custom Validator (with a bonus gotcha comments for date_format and date_before localization):
<?php namespace App\Services\Validation;
use Illuminate\Validation\Validator as BaseValidator;
/**
* Class for your custom validation functions
*/
class Validator extends BaseValidator {
public function validateDateNotInFuture($attribute, $value, $parameters)
{
// you could also test if the string is a date at all
// and if it matches your app specific format
// calling $this->validateDateFormat validator with your app's format
// loaded from \Config::get, but be careful -
// Laravel has hard-coded checks for DateFormat rule
// to extract correct format from it if it exists,
// and then use for validateBefore. If you have some unusual format
// and date_format has not been applied to the field,
// then validateBefore will give unpredictable results.
// Your best bet then is to override protected function
// getDateFormat($attribute) to return your app specific format
$tomorrow = date('your app date format here', strtotime("tomorrow"));
$parameters[0] = $tomorrow;
return $this->validateBefore($attribute, $value, $parameters);
}
}
ValidatorServiceProvider file:
<?php namespace App\Providers;
namespace App\Providers;
use App\Services\Validation\Validator;
use Illuminate\Support\ServiceProvider;
class ValidatorServiceProvider extends ServiceProvider{
public function boot()
{
\Validator::resolver(function($translator, $data, $rules, $messages)
{
return new Validator($translator, $data, $rules, $messages);
});
}
public function register()
{
}
}
And then just add a line to config/app.php:
'App\Providers\RouteServiceProvider',
'App\Providers\ValidatorServiceProvider', // your custom validation
In addition to what TheShiftExchange has said, if you look in that validation.php language file you'll see all of the different rules that you can specify. So for instance, if your validator has entries like this:
class ArticleValidator extends Validator
{
public static $rules = [
'create' => [
'title' => ['required'],
'slug' => ['required', 'regex:([a-z\0-9\-]*)']
]
];
}
Then your custom validation rules may look like this:
'custom' => array(
'company_article_type_id' => array(
'required' => 'The slug field is really important',
'exists' => 'The slug already exists',
),
),
Notice how the 'required' and 'exists' keys in the custom validation rules match those in the validator above.
I'm trying to set the prefix path for Zend Form's validators and filters automatically. I've done a lot of research and found many solutions, but I want these to always take effect, automatically. I know I can do this in the form:
$this->addElementPrefixPath('My_Validate', 'My/Validate/', 'validate');
or set them on an element such as
$input->addValidatorPrefixPath('Other_Namespace', 'Other/Namespace');
$input->addFilterPrefixPath('Foo_Namespace', 'Foo/Namespace');
but is there any way for these to automatically look into what's already set in the autoloader and/or be set in the bootstrap (or elsewhere), without having to set it ever again?
Here's my autoloader:
// Autoload libraries
$autoloader = Zend_Loader_Autoloader::getInstance();
$autoloader->registerNamespace('Lib1_')
->registerNamespace('Lib2_')
->registerNamespace('Lib3_');
Now, when I add a validator using
->addValidator('CustomValidator', false, 1)
I would like it to respect the hierarchy listed in the autoloader, falling back to Zend after that. I just haven't been able to find how to automatically bootstrap this kind of autoloading for the validators and filters.
Thanks!
The way I get settings like this into all of my Zend forms without having to repeat the code all over for each form is to create a base form class that extends Zend_Form, which in turn all of my other forms extend.
In the base form's constructor, I set up different decorators for various types of elements or customize the decorators for my application, specify prefix paths for helpers and validators and other things.
The important thing to note about this method, is you must call parent::__construct() as the very last line if your base form __construct method. The reason for this is because the Zend_Form::init() method gets called by Zend_Form::__construct() and nothing else from the constructor runs after that.
Here is an example:
<?php
class Application_Form_Base extends Zend_Form
{
// decorator spec for form elements like text, select etc.
public $elementDecorators = array(
'ViewHelper',
'Errors',
array('Description', array('tag' => 'p', 'class' => 'description', 'escape' => false)),
array('HtmlTag', array('class' => 'form-div')),
array('Label', array('class' => 'form-label', 'requiredSuffix' => '*'))
);
// decorator spec for checkboxes
public $checkboxDecorators = array(
'ViewHelper',
'Errors',
array('Label', array('class' => 'form-label', 'style' => 'display: inline', 'requiredSuffix' => '*', 'placement' => 'APPEND')),
array('HtmlTag', array('class' => 'form-div')),
array('Description', array('tag' => 'p', 'class' => 'description', 'escape' => false, 'placement' => 'APPEND')),
);
// decorator spec for submits and buttons
public $buttonDecorators = array(
'ViewHelper',
array('HtmlTag', array('tag' => 'div', 'class' => 'form-button'))
);
public function __construct()
{
// set the <form> decorators
$this->setDecorators(array(
'FormElements',
array('HtmlTag', array('tag' => 'div', 'class' => 'form')),
'Form'));
// set this as the default decorator for all elements added to the form
$this->setElementDecorators($this->elementDecorators, array('submit', 'button'), true);
// add prefix paths for decorators and validators
$this->addElementPrefixPath('My_Decorator', 'My/Decorator', 'decorator');
$this->addElementPrefixPath('My_Validator', 'My/Validator', 'validate');
parent::__construct();
// parent::__construct must be called last because it calls $form->init()
// and anything after it is not executed
}
}