I am using a stand alone Zend Form (I'm not using the full blown ZF2 MVC) and I have specified the following class to define the form:
use Zend\Form\Annotation;
/**
* #Annotation\Hydrator("Zend\Stdlib\Hydrator\ObjectProperty")
*/
class Student
{
/**
* #Annotation\Type("Zend\Form\Element\Text")
* #Annotation\Options({"label":"Student code"})
* #Annotations\Validator({"name":"Regex", "options":{"pattern":"/^[0-9]+$/"}})
* #Annotation\Required({"required":"true"})
*/
public $student_code;
}
This is the relevant code in my controller (simplified to only show the relevant parts)
public function createAction()
{
$request = $this->getRequest();
$student = new Student();
$builder = new AnnotationBuilder();
$form = $builder->createForm($student);
$form->bind($student);
$form->setData($request->getPost());
if ($form->isValid()) {
var_dump($form->getPost());
}
}
The problem is that when I submitt the form and with 'abc' as the value for student_code, the form is returning as valid. According to the Regex it should only accept numbers.
The required part works; the form is invalid if student_code is empty. My question is, what am I missing that the Regex is not working?
replace with:
#Annotation\Validator({"name":"Regex", "options":{"pattern":"/^[0-9]+$/"}})
Note that I have removed the s at the end from #Annotation
Related
I am trying to perform some validation for some query string parameters that were passed in. I want to do 3 things:
Check if firstname was passed.
If it was passed, validate that it is a string. Otherwise, throw an error.
If it isn't passed, assign a default name.
I want to re-use as much of the built-in Symfony validator functionality to do this and so far have something like the code below (but it is not working). Would anyone have suggestions?
Relevant References:
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Validator\Constraints\Collection
use Symfony\Component\Validator\Constraints\Type
Code:
public function testingAction(Request $request)
{
$parameters = $request->query->all();
// for this example, assume that $parameters contains 'firstname'=>123
$collectionConstraint = new Collection(array(
'firstname' => new Type(array('type'=>'string'))
);
$errors = $this->container->get('validator')->validate($parameters, $collectionConstraint);
return new Response('<html><body><pre>' . print_r($errors, TRUE) . '</pre></body></html>');
}
Symfony validation works on entity class. You need to create an entity class for your data with validation annotations.
// src/Entity/Author.php
// ...
use Symfony\Component\Validator\Constraints as Assert;
class Author
{
/**
* #Assert\NotBlank()
*/
public $name;
}
//then use this class for your data
use src/Entity/Auther.php;
public function testingAction(Request $request)
{
$parameters = $request->query->all();
$auther = new Auther();
$auther->setName($paramater['name']);
$errors = $this->container->get('validator')->validate($auther);
return new Response('<html><body><pre>' . print_r($errors, TRUE) . '</pre></body></html>');
}
Please follow the symfony link https://symfony.com/doc/current/validation.html
I've got a search form with some select boxes. I render it in the headnavi on every page using ebedded controllers. (http://symfony.com/doc/current/book/templating.html#embedding-controllers)
I want to use the form output to redirect to my list-view page like this:
/list-view/{city}/{category}?q=searchQuery
My form and the request is working well when I call the controller through a route, but unfortunately when I embed the controller, I'm stumblig over two problems. Like I've read here (Symfony 2 - Layout embed "no entity/class form" validation isn't working) my request isn't handeled by my form because of the sub-request. There is a solution in the answer, but its not very detailed.
The other problem, after fixing the first one, will be that I can't do a redirect from an embedded controller (Redirect from embedded controller).
Maybe anyone has an easier solution for having a form on every page that lets me do a redirect to its data?
Many thanks and greetings
Raphael
The answer of Symfony 2 - Layout embed "no entity/class form" validation isn't working is 100% correct, but we use contexts and isolate them, so an action which always uses the master request would break the rules. You have all requests (one master and zero or more subrequests) in the request_stack. Injecting Request $request into your controller action is the current request which is the subrequest with only max=3 (injecting the Request is deprecated now). Thus you have to use the 'correct' request.
Performing a redirection can be done in many ways, like return some JS script code to redirect (which is quite ugly imho). I would not use subrequests from twig because it's too late to start a redirection then, but make the subrequest in the action. I didn't test the code, but it should work. Controller::forward is your friend, since it duplicatest the current request for performing a subrequest.
Controller.php (just to see the implementation).
/**
* Forwards the request to another controller.
*
* #param string $controller The controller name (a string like BlogBundle:Post:index)
* #param array $path An array of path parameters
* #param array $query An array of query parameters
*
* #return Response A Response instance
*/
protected function forward($controller, array $path = array(), array $query = array())
{
$path['_controller'] = $controller;
$subRequest = $this->container->get('request_stack')->getCurrentRequest()->duplicate($query, null, $path);
return $this->container->get('http_kernel')->handle($subRequest, HttpKernelInterface::SUB_REQUEST);
}
YourController.php
public function pageAction() {
$formResponse = $this->forward('...:...:form'); // e.g. formAction()
if($formResponse->isRedirection()) {
return $formResponse; // just the redirection, no content
}
$this->render('...:...:your.html.twig', [
'form_response' => $formResponse
]);
}
public function formAction() {
$requestStack = $this->get('request_stack');
/* #var $requestStack RequestStack */
$masterRequest = $requestStack->getCurrentRequest();
\assert(!\is_null($masterRequest));
$form = ...;
$form->handleRequest($masterRequest);
if($form->isValid()) {
return $this->redirect(...); // success
}
return $this->render('...:...:form.html.twig', [
'form' => $form->createView()
]);
}
your.html.twig
{{ form_response.content | raw }}
my sniff doesn't work and doesn't recognize the property private $testvar. I want to make a Doc-Block mandatory there.
When I run code sniffer, the process method doesn't seem to be used. I added some echos there before.
Does the token T_PROPERTY exist? I cannot find it on php manual http://php.net/manual/en/tokens.php
Yet, in the squiz lab source code T_PROPERTY is used.
<?php
/**
* Extension for the pear class comment sniff.
*
*/
/**
* Extension for the pear class comment sniff.
*
*/
class XYZ_Sniffs_Commenting_PropertyCommentSniff implements PHP_CodeSniffer_Sniff
{
private $testvar = 1;
/**
* Returns an array of tokens this test wants to listen for.
*
* #return array
*/
public function register()
{
return array(T_PROPERTY);
}
/**
* Checks the property comments.
*
* #param PHP_CodeSniffer_File $phpcsFile the file object
* #param int $stackPtr the stack pointer
*
* #return void
*/
public function process(PHP_CodeSniffer_File $phpcsFile, $stackPtr)
{
$tokens = $phpcsFile->getTokens();
$find = PHP_CodeSniffer_Tokens::$scopeModifiers;
$find[] = T_WHITESPACE;
$find[] = T_STATIC;
$commentEnd = $phpcsFile->findPrevious($find, ($stackPtr - 1), null, true);
if ($tokens[$commentEnd]['code'] !== T_DOC_COMMENT_CLOSE_TAG
&& $tokens[$commentEnd]['code'] !== T_COMMENT
) {
$phpcsFile->addError('Missing property doc comment', $stackPtr, 'Missing');
$phpcsFile->recordMetric($stackPtr, 'Function has property comment', 'no');
return;
} else {
$phpcsFile->recordMetric($stackPtr, 'Function has property comment', 'yes');
}
}
}
Thanks for your help :).
The T_PROPERTY token is only used when checking JavaScript files. It doesn't exist for PHP files.
For PHP files, you'll want to use the AbstractVariableSniff helper. Here is a sniff that checks comments of member vars: https://github.com/squizlabs/PHP_CodeSniffer/blob/master/CodeSniffer/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php
Notice how it extends PHP_CodeSniffer_Standards_AbstractVariableSniff and then only implements the processMemberVar() method. It leaves the processVariable() and processVariableInString() methods empty because it doesn't care about regular variables inside the code.
Also note that if you are writing commenting sniffs, the comment parser is completely different in the 2.0 version (currently in beta but due to go stable any week now). Take a look at the new version of the above sniff here: https://github.com/squizlabs/PHP_CodeSniffer/blob/phpcs-fixer/CodeSniffer/Standards/Squiz/Sniffs/Commenting/VariableCommentSniff.php
I am new to joomla component development(J3 , MVC) and i am trying to create a custom server side form validation rule.
I added validate="machinename" to my forms field and created a the file models\rules\machinename.php
defined('_JEXEC') or die('Restricted access');
jimport('joomla.form.formrule');
class JFormRuleMachinename extends JFormRule
{
protected $regex = '/^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$/';
}
I have a empty controller in controllers\field.php
defined('_JEXEC') or die('Restricted access');
// import Joomla controllerform library
jimport('joomla.application.component.controllerform');
class SampleControllerField extends JControllerForm
{
}
and the model is in models\field.php
defined('_JEXEC') or die('Restricted access');
// import Joomla modelform library
jimport('joomla.application.component.modeladmin');
/**
* HelloWorld Model
*/
class SampleModelField extends JModelAdmin
{
public function getTable($type = 'Field', $prefix = 'SampleTable', $config = array())
{
return JTable::getInstance($type, $prefix, $config);
}
/**
* Method to get the record form.
*
* #param array $data Data for the form.
* #param boolean $loadData True if the form is to load its own data (default case), false if not.
* #return mixed A JForm object on success, false on failure
* #since 2.5
*/
public function getForm($data = array(), $loadData = true)
{
// Get the form.
$form = $this->loadForm('com_sample.field', 'field',
array('control' => 'jform', 'load_data' => $loadData));
if (empty($form))
{
return false;
}
return $form;
}
/**
* Method to get the data that should be injected in the form.
*
* #return mixed The data for the form.
* #since 2.5
*/
protected function loadFormData()
{
// Check the session for previously entered form data.
$data = JFactory::getApplication()->getUserState('com_sample.edit.field.data', array());
if (empty($data))
{
$data = $this->getItem();
}
return $data;
}
}
my components name is com_sample and everything was working fine (new,edit,delete) but then i added the validation rule to the form's field and now i am getting a error when submitting the form :
JForm::validateField() rule `machinename` missing.
my best guess is that i have a mistake in naming or the file location but i am not sure and can't find anything with googleing .
so help me pliz ...
Find the solution myself, it seems that you need to add the rules folder pathto the form definition so :
<form addrulepath="/administrator/components/com_sample/models/rules">
this solved my problem .
I was struggling with this problem. I read the error as meaning that Joomla couldn't find the rule file, but when I single-stepped through the core I realised that after loading the rule file, Jommla checks that an appropriately named class is within the rule. I'd introduced a typo to the class name. So my advice to anyone struggling with server-side validation is to check the rule file is where you'd expect, AND that the class name is correct. Obvious I know, but it took me ages to figure.
I'm currently facing a challenge with SonataAdminBundle, one-to-many relationships and file uploads. I have an Entity called Client and one called ExchangeFile. One Client can have several ExchangeFiles, so we have a one-to-many relationship here. I'm using the VichUploaderBundle for file uploads.
This is the Client class:
/**
* #ORM\Table(name="client")
* #ORM\Entity()
* #ORM\HasLifecycleCallbacks
*/
class Client extends BaseUser
{
// SNIP
/**
* #ORM\OneToMany(targetEntity="ExchangeFile", mappedBy="client", orphanRemoval=true, cascade={"persist", "remove"})
*/
protected $exchangeFiles;
// SNIP
}
and this is the ExchangeFile class:
/**
* #ORM\Table(name="exchange_file")
* #ORM\Entity
* #Vich\Uploadable
*/
class ExchangeFile
{
// SNIP
/**
* #Assert\File(
* maxSize="20M"
* )
* #Vich\UploadableField(mapping="exchange_file", fileNameProperty="fileName")
*/
protected $file;
/**
* #ORM\Column(name="file_name", type="string", nullable=true)
*/
protected $fileName;
/**
* #ORM\ManyToOne(targetEntity="Client", inversedBy="exchangeFiles")
* #ORM\JoinColumn(name="client_id", referencedColumnName="id")
*/
protected $client;
// SNIP
}
In my ClientAdmin class, i added the exchangeFiles field the following way:
protected function configureFormFields(FormMapper $formMapper)
{
$formMapper
// SNIP
->with('Files')
->add('exchangeFiles', 'sonata_type_collection', array('by_reference' => false), array(
'edit' => 'inline',
'inline' => 'table',
))
// SNIP
}
This allows for inline editing of various exchange files in the Client edit form. And it works well so far: .
The Problem
But there's one ceveat: When i hit the green "+" sign once (add a new exchange file form row), then select a file in my filesystem, then hit the "+" sign again (a new form row is appended via Ajax), select another file, and then hit "Update" (save the current Client), then the first file is not persisted. Only the second file can be found in the database and the file system.
As far as I could find out, this has the following reason: When the green "+" sign is clicked the second time, the current form is post to the web server, including the data currently in the form (Client and all exchange files). A new form is created and the request is bound into the form (this happens in the AdminHelper class located in Sonata\AdminBundle\Admin):
public function appendFormFieldElement(AdminInterface $admin, $subject, $elementId)
{
// retrieve the subject
$formBuilder = $admin->getFormBuilder();
$form = $formBuilder->getForm();
$form->setData($subject);
$form->bind($admin->getRequest()); // <-- here
// SNIP
}
So the entire form is bound, a form row is appended, the form is sent back to the browser and the entire form is overwritten by the new one. But since file inputs (<input type="file" />) cannot be pre-populated for security reasons, the first file is lost. The file is only stored on the filesystem when the entity is persisted (I think VichUploaderBundle uses Doctrine's prePersist for this), but this does not yet happen when a form field row is appended.
My first question is: How can i solve this problem, or which direction should i go? I would like the following use case to work: I want to create a new Client and I know I'll upload three files. I click "New Client", enter the Client data, hit the green "+" button once, select the first file. Then i hit the "+" sign again, and select the second file. Same for the third file. All three files should be persisted.
Second question: Why does Sonata Admin post the entire form when I only want to add a single form row in a one-to-many relationship? Is this really necessary? This means that if I have file inputs, all files present in the form are uploaded every time a new form row is added.
Thanks in advance for your help. If you need any details, let me know.
Further to my comment about SonataMediaBundle...
If you do go this route, then you'd want to create a new entity similar to the following:
/**
* #ORM\Table
* #ORM\Entity
*/
class ClientHasFile
{
/**
* #var integer $id
*
* #ORM\Column(name="id", type="integer")
* #ORM\Id
* #ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* #var Client $client
*
* #ORM\ManyToOne(targetEntity="Story", inversedBy="clientHasFiles")
*/
private $client;
/**
* #var Media $media
*
* #ORM\ManyToOne(targetEntity="Application\Sonata\MediaBundle\Entity\Media")
*/
private $media;
// SNIP
}
Then, in your Client entity:
class Client
{
// SNIP
/**
* #var \Doctrine\Common\Collections\ArrayCollection
*
* #ORM\OneToMany(targetEntity="ClientHasFile", mappedBy="client", cascade={"persist", "remove"}, orphanRemoval=true)
*/
protected $clientHasFiles;
public function __construct()
{
$this->clientHasFiles = new ArrayCollection();
}
// SNIP
}
... and your ClientAdmin's configureFormFields:
protected function configureFormFields(FormMapper $form)
{
$form
// SNIP
->add('clientHasFiles', 'sonata_type_collection', array(
'required' => false,
'by_reference' => false,
'label' => 'Media items'
), array(
'edit' => 'inline',
'inline' => 'table'
)
)
;
}
... and last but not least, your ClientHasFileAdmin class:
class ClientHasFileAdmin extends Admin
{
/**
* #param \Sonata\AdminBundle\Form\FormMapper $form
*/
protected function configureFormFields(FormMapper $form)
{
$form
->add('media', 'sonata_type_model_list', array(), array(
'link_parameters' => array('context' => 'default')
))
;
}
/**
* {#inheritdoc}
*/
protected function configureListFields(ListMapper $list)
{
$list
->add('client')
->add('media')
;
}
}
I've figured out, that it could be possible to solve this problem by remembering the file inputs content before the AJAX call for adding a new row. It's a bit hacky, but it's working as I'm testing it right now.
We are able to override a template for editing - base_edit.html.twig. I've added my javascript to detect the click event on the add button and also a javascript after the row is added.
My sonata_type_collection field is called galleryImages.
The full script is here:
$(function(){
handleCollectionType('galleryImages');
});
function handleCollectionType(entityClass){
let clonedFileInputs = [];
let isButtonHandled = false;
let addButton = $('#field_actions_{{ admin.uniqid }}_' + entityClass + ' a.btn-success');
if(addButton.length > 0){
$('#field_actions_{{ admin.uniqid }}_' + entityClass + ' a.btn-success')[0].onclick = null;
$('#field_actions_{{ admin.uniqid }}_' + entityClass + ' a.btn-success').off('click').on('click', function(e){
if(!isButtonHandled){
e.preventDefault();
clonedFileInputs = cloneFileInputs(entityClass);
isButtonHandled = true;
return window['start_field_retrieve_{{ admin.uniqid }}_'+entityClass]($('#field_actions_{{ admin.uniqid }}_' + entityClass + ' a.btn-success')[0]);
}
});
$(document).on('sonata.add_element', '#field_container_{{ admin.uniqid }}_' + entityClass, function() {
refillFileInputs(clonedFileInputs);
isButtonHandled = false;
clonedFileInputs = [];
handleCollectionType(entityClass);
});
}
}
function cloneFileInputs(entityClass){
let clonedFileInputs = [];
let originalFileInputs = document.querySelectorAll('input[type="file"][id^="{{ admin.uniqid }}_' + entityClass + '"]');
for(let i = 0; i < originalFileInputs.length; i++){
clonedFileInputs.push(originalFileInputs[i].cloneNode(true));
}
return clonedFileInputs;
}
function refillFileInputs(clonedFileInputs){
for(let i = 0; i < clonedFileInputs.length; i++){
let originalFileInput = document.getElementById(clonedFileInputs[i].id);
originalFileInput.replaceWith(clonedFileInputs[i]);
}
}
I tried many different approaches and workaround and in the end I found out that the best solution in the one described here https://stackoverflow.com/a/25154867/4249725
You just have to hide all the unnecessary list/delete buttons around the file selection if they are not needed.
In all other cases with file selection directly inside the form you will face some other problems sooner or later - with form validation, form preview etc. In all these case input fields will be cleared.
So using media bundle and sonata_type_model_list is probably the safest option despite quite a lot of overhead.
I'm posting it in case someone is searching for the solution the way I was searching.
I've found also some java-script workaround for this exact problem. It worked basically changing names of file inputs when you hit "+" button and then reverting it back.
Still in this case you are still left with the problem of re-displaying the form if some validation fails etc. so I definitely suggest media bundle approach.