Reusable action Yii2, failed load form view - php

I want to create a reusable action inside a controller.
Yii2 usually creates the following views:
update -> render form
create -> render form
The problem is, I failed to load this form.
Yii2 throw an error Call to a member function isAttributeRequired() on string
Here is the list of My apps directory.
modules
finance
actions
- BuatPengeluaranDiBukuBankAction.php
controllers
- BukuBankController.php
views
- buku-bank
* create-pengeluaran-by-kasbon.php
* _form-pengeluaran-by-kasbon
I will describe the following code.
BuatPengeluaranDiBukuBankAction.php
class BuatPengeluaranDiBukuBankAction extends Action {
public $modelClass;
public $modelsDetailClass;
public function run() {
$request = Yii::$app->request;
if($this->modelClass->load($request->post)){
... save here & redirect
}
### Calling view
return $this->controller->render('create-pengeluaran-by-kasbon', [
'model' => $this->modelClass,
'modelsDetail' => empty($this->modelsDetailClass) ? [new BukuBankDetail()] : $this->modelsDetailClass,
]);
}
}
BukuBankController.php
class BukuBankController extends Controller {
public function actions() {
return ArrayHelper::merge(parent::actions(),[
'create-pengeluaran-by-kasbon' => [
'class' => BuatPengeluaranDiBukuBankAction::class,
'modelClass' => BukuBank::class,
'modelsDetailClass' => BukuBankDetail::class
]
]);
}
}
create-pengeluaran-by-kasbon.php
<div class="buku-bank-create-by-kasbon">
<?php echo $this->render('_form-pengeluaran-by-kasbon', [ # Failed to load
'model' => $model,
'modelsDetail' => $modelsDetail,
]) ?>
</div>
_form-pengeluaran-by-kasbon
<?php $form = ActiveForm::begin([
'id' => 'dynamic-form',
/*'type' => ActiveForm::TYPE_HORIZONTAL,
'formConfig' => ['labelSpan' => 3, 'deviceSize' => ActiveForm::SIZE_SMALL]*/
]); ?>
.... lot of field here
<?php ActiveForm::end(); ?>

What is happening
'modelClass' => BukuBank::class,
'modelsDetailClass' => BukuBankDetail::class
are passed as strings and not objects to your action class so before use you need to instantiate (create) this models (objects) see code below with comments.
class BuatPengeluaranDiBukuBankAction extends Action {
public $modelClass;
public $modelsDetailClass;
public function run() {
$request = Yii::$app->request;
// create instances of models here
$model = Yii::createObject($this->modelClass);
$modelsDetails = Yii::createObject($this->modelsDetailClass);
// now you can use models
if($model->load($request->post)){
... save here & redirect
}
### Calling view
return $this->controller->render('create-pengeluaran-by-kasbon', [
'model' => $model,
'modelsDetail' => $modelsDetails,
]);
}
}

Related

Backpack for laravel -> I can't get + Add Inline Create

I want to use the Inline Create functionality of Backpack for Laravel but the button "Add +" doesn't shows up.
I have a 1 to n relationship
The primary model is Inscription and the secondary is InscriptionProduct
This is my code:
Models\Inscription
...
class Inscription extends Model
{
public function product() {
return $this->hasMany('App\Models\InscriptionProduct');
}
...
Models\InscriptionProduct
...
class InscriptionProduct extends Model
{
public function inscription()
{
return $this->belongsTo(Inscription::class);
}
...
Http\Controllers\Admin\InscriptionProductCrudController
...
class InscriptionProductCrudController extends CrudController
{
use \Backpack\CRUD\app\Http\Controllers\Operations\ListOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\CreateOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\UpdateOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\DeleteOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\ShowOperation;
use \Backpack\CRUD\app\Http\Controllers\Operations\InlineCreateOperation;
...
Http\Controllers\Admin\InscriptionCrudController
...
class InscriptionCrudController extends CrudController
{
protected function setupCreateOperation()
{
...
$this->crud->addField([
'type' => "relationship",
'name' => 'product',
'ajax' => true,
'inline_create' => true,
// 'data_source' => backpack_url('/admin/inscription-product/inline/create'),
// 'data_source' => backpack_url('inscription/fetch/inscription-product'),
// 'inline_create' => [ 'entity' => 'inscriptionproduct' ]
// These 3 commented lines are alternatives also tried with no results
]);
...
The Relationship is well constructed because I can see the Products I have created via the CRUD of InscriptionProduct
What am I missing?
I solve it to, in your main CrudController, you should call the InLineCreate just below CreateOperation.
use \Backpack\CRUD\app\Http\Controllers\Operations\CreateOperation { store as traitStore; }
use \Backpack\CRUD\app\Http\Controllers\Operations\InlineCreateOperation { store as traitStore; }
Also in my secondary CrudController I had to create a fetch function, something I did
protected function fetchFeatures()
{
return $this->fetch([
'model' => \App\Models\Features::class, // required
'searchable_attributes' => ['name', 'slug'],
'query' => function($model) {
return $model;
} // to filter the results that are returned
]);
return $this->traitFetch();
}

Route is redirecting before calling the controller method

I am using Laravel 5.5.40 along with the Zizaco\Entrust Pacakge
In my routes/web.php file i have the following route setup.
Route::group(['prefix' => 'order'], function() {
Route::get('', 'OrderController#getMe');
});
It is supposed to call the getMe() method inside the OrderController.php but it instead redirects to www.mydomain.co.uk/home
namespace App\Http\Controllers;
class OrderController extends Controller
{
public function getMe() {
return "You got me!";
}
}
As a test, I added a __construct function to the OrderController.php to see if the class was even been loaded.
public function __construct() {
dd("Testing");
}
When accessing www.mydomain.co.uk/order i now get
"Testing"
I can't seem to work out why it is not running the getMe() method. Could anyone possibly shine some light on this please?
I have also tried changing the route to use ClientController#list which works fine.
Contents of ClientController.php
namespace App\Http\Controllers;
use App\Client;
class ClientController extends Controller
{
public function __construct() {
//
}
// Display all the clients
public function list() {
$tabContent = [
'display_type' => 'list',
'data' => Client::orderBy('name', 'asc')->get(),
'view_params' => [
'columns' => [
'name' => 'Client Name',
'address_line_1' => 'Address Line 1',
'town' => 'Town',
'county' => 'County',
'post_code' => 'Post Code'
],
'links' => 'client',
'controls' => True
]
];
return view('tables.list', ['data' => $tabContent]);
}
}
It has become apparent that if the controller does not have the constructor function in it, it will automatically redirect to the root of the URI with no error.
public function __construct() {
//
}

Zend Form without binding object and access to fieldset data

I have Zend Framework 2 Form:
$form = new Form();
$form->add(
[
'name' => 'input1',
'type' => 'Text',
]
);
$fieldset1 = new Fieldset();
$fieldset1->setName('field1');
$fieldset1->add(
[
'name' => 'input2',
'type' => 'Text',
]
);
and controller for it:
$request = $this->getRequest();
if ($request->isPost()) {
$form->setData($request->getPost());
if ($form->isValid()) {
$data = $form->getData();
var_dump($this->params()->fromPost(),$data);
exit;
}
}
and problem is that when i dump values i get this:
array (size=3)
'input1' => string 'a' (length=1)
'input2' => string 'b' (length=1)
array (size=3)
'input1' => string 'a' (length=1)
'field1' =>
array (size=1)
'input2' => null
So what i do wrong? Because now in "field2" key i get "nulll". how i can get access to fieldset(s) data (after filters, validation etc) in that case?
Update: as i see, when i add to POST
<input name="field1[input2]" value="test" />
i get expected result. but why zendform not generate html like that, but (wrongly) generate:
<input name="input2" />
what i do wrong?
here is a complete more or less simple example with entities, input filters, hydrators and validators für zf2 form use with fieldsets.
First set up the fieldset class you want to use.
namespace Application\Form;
use Zend\Filter\StripTags;
use Zend\Form\Fieldset;
use Zend\Form\Element\Text;
use Zend\InputFilter\InputFilterProviderInterface;
class MyFieldset extends Fieldset implements InputFilterProviderInterface
{
/**
* #see \Zend\Form\Element::init()
*/
public function init()
{
$this->add([
'name' => 'input2',
'type' => Text::class,
'required' => true,
'attributes' => [
'id' => 'input2',
'required' => true,
],
'options' => [
'label' => 'The label for input2 of this fieldset',
],
]);
}
/**
* #see \Zend\InputFilter\InputFilterProviderInterface::getInputFilterSpecification()
*/
public function getInputFilterSpecification()
{
return [
'input2' => [
'required' => true,
'filters' => [
[
'name' => StripTags::class,
],
],
],
];
}
}
Your fieldset class defines all input elements within the fieldset. I encurage you to work with entity classes and factories. this is also the reason this example works with the init method. The init method is called after the constructor of the class. While using factories you can use the constructor for defining stuff you need for your fieldset or form class. For example depending input fields and so on.
Next you should write an entity for your fieldset.
namespace Application\Entity;
class MyFieldsetEntity
{
protected $input2;
public function getInput2()
{
return $this->input2;
}
public function setInput2($input2)
{
$this->input2 = $input2;
return $this;
}
}
This simple entity class will handle the data you have sent to your controller. One of the benefits of a entity class is, that you can define default values in it. If the post data should be empty for some reason, the entity can return default values. Let 's put it all together in a factory for your fieldset.
namespace Application\Form\Service;
class MyFieldsetFactory
{
public function __invoke(ContainerInterface $container)
{
$hydrator = new ClassMethods(false);
$entity = new MyFieldsetEntity();
return (new MyFieldset())
->setObject($entity)
->setHydrator($hydrator);
}
}
Why is using a factory smart? Because you can use all the favors of an object orientated environment. You can define all the stuff you need in a factory. for this purpose we create a fieldset instance with an entity and a hydrator. This will hydrate the fieldset with the filtered and validated data.
All that we need now is the form and an entity for the form.
namespace ApplicationForm;
use Zend\Form\Element\Text;
use Zend\Form\Form;
class MyForm extends Form
{
public function __construct($name = null, array $options = [])
{
parent::__construct($name, $options);
$this->setAttribute('method', 'post');
$this->add([
'name' => 'input1',
'type' => Text::class,
'required' => true,
'attributes' => [
'id' => 'input2',
'required' => true,
],
'options' => [
'label' => 'The label for input2 of this fieldset',
],
]);
// here goes your fieldset (provided, that your fieldset class is defined in the form elements config in your module.config.php file)
$this->add([
'name' => 'fieldset1',
'type' => MyFieldset::class,
]);
}
}
That 's all for your form. This form is implementing your fieldset. That 's all. Now we need a validator and an entity for this form.
namespace Application\Entity;
class MyFormEntity
{
protected $input1;
// we will hydrate this property with the MyFieldsetEntity
protected $fieldset1;
public function getInput1()
{
return $this->input1;
}
public function setInput1($input1)
{
$this->input1 = $input1;
return $this;
}
public function getFieldset1()
{
return $fieldset1;
}
public function setFieldset1($fieldset1)
{
$this->fieldset1 = $fieldset1;
return $this;
}
}
... and finally the input filter class for your form. An input filter filters and validates your form data. You should use always an input filter for security reasons and many more.
namespace Application\InputFilter;
use Zend\InputFilter\InputFilter;
use Zend\Filter\StripTags;
use Zend\Filter\StringTrim;
class MyFormInputFilter extends InputFilter
{
public function __construct()
{
$this->add([
'name' => 'input1',
'required' => true,
'filters' => [
[
'name' => StripTags::class,
],
[
'name' => StringTrim::class,
],
],
]);
}
}
Simple, hm? This input filter class just sets some input filters for your input 1 form element. The fieldset element is filtered by itself because it implements the InputFilterProviderInterface interface. You don 't hav to define more in the input filter class for your form.
Put it together in a factory ...
namespace Application\Form\Service;
class MyFormFactory
{
public function __invoke(ContainerInterface $container)
{
$entity = new MyFormEntity();
$inputFilter = new MyFormInputFilter();
$hydrator = (new ClassMethods(false))
->addStrategy('fieldset1', new Fieldset1Strategy());
$form = (new MyForm())
->setHydrator($hydrator)
->setObject($entity)
->setInputFilter($inputFilter);
return $form;
}
}
This is the factory for your form. This factory contains a special feature. It adds a hydrator strategy to your hydrator instance. this strategy will hydrate your entity with the fieldset data, if there is a 'fieldset1' key in your post array.
This will be the hydrator strategy class ...
namespace Application\Hydrator\Strategy;
use Zend\Hydrator\Strategy\DefaultStrategy;
use Zend\Hydrator\ClassMethods;
use Application\Entity\MyFieldsetEntity;
class Fieldset1Strategy extends DefaultStrategy
{
public function hydrate($value)
{
if (!$value instanceof MyFieldsetEntity) {
return (new ClassMethods(false))->hydrate($value, new MyFieldsetEntity());
}
return $value;
}
}
This strategy will add the MyFieldsetEntity to your form entity.
The last step is defining all that stuff in the config file module.config.php
// for the forms config provides the form elements key
'form_elements' => [
'factories' => [
YourForm::class => YourFormFactory::class,
YourFormFieldset::class => YourFormFactory::class,
]
],
// can be accessed with $container->get('FormElementsManager')->get(YourFormFieldset::class);
Usage Example
This is a small example how to use it in a controller.
class ExampleController extends AbstractActionController
{
protected $form;
public function __construct(Form $form)
{
$this->form = $form;
}
public function indexAction()
{
if ($this->getRequest()->isPost()) {
$this->form->setData($this->getRequest()->getPost());
if ($this->form->isValid()) {
$data = $this->form->getData();
\Zend\Debug\Debug::dump($data);
die();
// output will be
// MyFormEntity object
// string input1
// MyFieldsetEntity fieldset1
// string input2
// for fetching the filtered data
// $data->getInput1();
// $data->getFieldset1()->getInput2();
}
}
return [
'form' => $this->form,
];
}
}
In your view / template you can display the form with the different form view helpers zf2 provides.
$form = $this->form;
$form->setAttribute('action', $this->url('application/example'));
$form->prepare();
echo $this->form()->openTag($form);
// outputs the single text input element
echo $this->formRow($form->get('input1'));
// outputs the complete fieldset
echo $this->formCollection($form->get('fieldset1'));
Sure, this answer is a bit complex. But I encurage you to have a try. Once implemented in your application, this kind of form management is the easiest way you can use. Keep in mind, that just handling the raw post data can be insecure as hell. If you want the filtered data with the benefit of objects it is recommended using entities, input filters and all the other cool stuff zend framework comes with.
You have not added the fieldset to the form.
$form->add($fieldset1);
You forgot to prepare the form. It's in $form->prepare() that the names for the elements are changed to include the prefix for the fieldset.
If you use the "form" view helper, that will prepare the form for you. If you don't, you'll have to call "prepare" it yourself, for example in the view, just before you output the open tag:
$this->form->prepare();

Form custom elements with factories

We are used to work with ZF2, but for our last project, we decided to start with ZF3.
Now I am facing a problem in the form creation.
What I want to do is to create a custom select populated with values retrieved from database.
What I did in ZF2 was creating a class extending a select, with the ServiceLocatorAwareInterface, like:
class ManufacturerSelect extends Select implements ServiceLocatorAwareInterface {
public function init() {
$manufacturerTable = $this->getServiceLocator()->get('Car\Model\ManufacturerTable');
$valueOptions = [];
foreach ($manufacturerTable->fetchAll() as $manufacturer) {
$valueOptions[$manufacturer->getManufacturerId()] = $manufacturer->getName();
}
$this->setValueOptions($valueOptions);
}
public function getServiceLocator() {
return $this->serviceLocator;
}
public function setServiceLocator(ServiceLocatorInterface $serviceLocator) {
$this->serviceLocator = $serviceLocator;
}
}
Then, to use it in a form, it was enough to give the full name
$this->add(
array(
'name' => 'manufacturer_id',
'type' => 'Car\Form\Element\ManufacturerSelect'
)
);
Now this is not possible anymore, since the service locator was removed and the use of factories is necessary, but I'm struggling to find how to do the same thing.
Keeping in mind to use factories, I tried this configuration in module.config.php:
'form_elements' => [
'factories' => [
'Car\Form\Element\ManufacturerSelect' => function ($services) {
$manufacturerTable = $services->get('Car\Model\ManufacturerTable');
return new ManufacturerSelect($manufacturerTable);
},
'Car\Form\CarForm' => function ($services) {
$manufacturerTable = $services->get('Car\Model\ManufacturerTable');
return new CarForm($manufacturerTable, 'car-form');
}
]
]
Result: factory of CarForm is always called, but factory of ManufacturerSelect is not.
A simple solution would be to populate the select directly in the form class, but I would prefer to use the factory for the element and reuse it everywhere I want, like I was doing in ZF2.
Does anyone already encountered this problem and found a solution?
Do you add that element in "__construct" function? If so try "init"
EDIT:
First of all you don't need to create a custom select to fill in it via database. Just create a form with factory, fetch data from db in factory and pass to form. And use the data in form class as select's value options.
$this-add([
'type' => Element\Select:.class,
'name' => 'select-element'
'options' => [
'label' => 'The Select',
'empty_option' => 'Please choose one',
'value_options' => $this-dataFromDB
]
]);
If you create form as:
new MyForm();
Form Element Manager doesn't trigger custom elements' factories. But;
$container->get('FormElementManager')->get(MyForm::class);
triggers custom elements' factories. Here's a working example. It's working on ZF3.
Config:
return [
'controllers' => [
'factories' => [
MyController::class => MyControllerFactory::class
]
],
'form_elements' => [
'factories' => [
CustomElement::class => CustomElementFactory::class,
MyForm::class => MyFormFactory::class,
]
]
];
don't forget to add 'Zend\Form' to application config's 'modules'.
Element:
class CustomElement extends Text
{
}
Element Factory:
class CustomElementFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
echo 'element factory triggered';
return new CustomElement();
}
}
Fieldset/Form:
class MyForm extends Form
{
public function init()
{
$this
->add([
'type' => CustomElement::class,
'name' => 'name',
'options' => [
'label' => 'label',
],
])
;
}
}
Fieldset/Form Factory:
class MyFormFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
echo 'form factory triggered';
return new MyForm();
}
}
Controller's Factory:
class MyControllerFactory implements FactoryInterface
{
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
echo 'controller factory triggered';
return new MyController(
$container->get('FormElementManager')->get(MyForm::class);
);
}
}

Registering custom Form View Helper

With ZF2, it was very simple to register custom view helpers for custom form elements.
You could simply create an element like such:
use Zend\Form\Element;
class Recaptcha extends Element
{
protected $attributes = [
'type' => 'recaptcha',
];
protected $secret;
public function getSecret()
{
return $this->secret;
}
public function __construct($secret)
{
parent::__construct();
$this->secret = $secret;
}
}
Create a matching helper:
use Zend\Form\ElementInterface;
use Zend\Form\View\Helper\FormElement;
class Recaptcha extends FormElement
{
public function render(ElementInterface $element)
{
return '<div class="form-group">
<div id="register_recaptcha">
<div class="g-recaptcha" data-sitekey="' . $element->getSecret() . '"></div>
</div>
</div>
<script src="//www.google.com/recaptcha/api.js"></script>';
}
}
And then wire it up at config:
return [
'form_elements' => [
'factories' => [
Recaptcha::class => RecaptchaFactory::class,
],
],
'view_helpers' => [
'invokables' => [
'recaptcha' => RecaptchaHelper::class,
],
],
];
IIRC, you would have to wire it up in the Bootstrap too
public function onBootstrap($e)
{
$application = $e->getApplication();
$services = $application->getServiceManager();
$services->get('ViewHelperManager')->get('FormElement')->addType('recaptcha', 'recaptcha');
}
Upgrading a project from ZF2 to ZF3, the custom element now appears as a textfield.
If I call the helper directly on the field, it renders properly:
{{ recaptcha( user_form.get('recaptchafield') ) | raw }}
It's the automatic association that's seemingly vanished. Such that calling formRow on each doesn't invoke the helper.
Anyone have the quick fix? Hopeful to save myself from reviewing the actual zend-form and zend-view code.
Thank you!
I had the same issue and I resolved it by replacing
$services->get('ViewHelperManager')->get('FormElement')->addType('recaptcha', 'recaptcha');
with
$services->get('ViewHelperManager')->get('FormElement')->addClass(Recaptcha::class, RecaptchaHelper::class);
The config also needed some adaption. It now reads like this:
return [
'form_elements' => [
'factories' => [
Recaptcha::class => RecaptchaFactory::class,
],
],
'view_helpers' => [
'invokables' => [
RecaptchaHelper::class => RecaptchaHelper::class,
],
],
];
Hope that helps someone else find the issue faster ;)
Invokables no longer exist in ZF3. You need to move your recatpcha view helper to the factories key instead and wire it up to Zend\ServiceManager\Factory\InvokableFactory::class

Categories