Dependency injection inside model factory - dependency-injection

This is my first question, so I would also appreciate hints on how to ask properly.
So, In my Laravel app, I have a database table with users. For start, I wanted to have a model factory for it. So I took a standard code from laravel doc page:
$factory->define(App\User::class, function (Faker\Generator $faker) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => bcrypt(str_random(10)),
'remember_token' => str_random(10),
];
});
And I changed it to:
$factory->define(App\User::class,
function(Faker\Generator $faker) {
return [
'name' => $faker->name(),
'email' => $faker->safeEmail(),
'password' => bcrypt(str_random(10)),
'phone_number' => $faker->phoneNumber(),
'remember_token' => str_random(10),
'account_type' => 0,
];
});
So far, everything works. But I wanted it to be more sophisticated, and I decided to use more specific kind of Faker class, to generate Italian data. I changed it to:
$factory->define(App\User::class,
function(Faker\Generator $faker,
Faker\Provider\it_IT\PhoneNumber $fakerITPN,
Faker\Provider\it_IT\Person $fakerITPER,
Faker\Provider\it_IT\Internet $fakerITInt) {
return [
'name' => $fakerITPER->name(),
'email' => $fakerITInt->safeEmail(),
'password' => bcrypt(str_random(10)),
'phone_number' => $fakerITPN->phoneNumber(),
'remember_token' => str_random(10),
'account_type' => 0,
];
});
In seeder class I wrote:
factory(App\User::class)->create();
And then, after I used Artisan, command:
artisan migrate:refresh --seed -vvv
I get following error (just the head, for clearance):
[ErrorException]
Argument 2 passed to Illuminate\Database\Eloquent\Factory::{closure}() must be an instance of Faker\Provider\it_IT\PhoneNumber, array given
Exception trace:
() at /home/vagrant/php/housing/database/factories/ModelFactory.php:19
Illuminate\Foundation\Bootstrap\HandleExceptions->handleError() at /home/vagrant/php/housing/database/factories/ModelFactory.php:19
Illuminate\Database\Eloquent\Factory::{closure}() at n/a:n/a
call_user_func() at /home/vagrant/php/housing/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:130
Illuminate\Database\Eloquent\FactoryBuilder->Illuminate\Database\Eloquent\{closure}() at /home/vagrant/php/housing/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Model.php:2308
Illuminate\Database\Eloquent\Model::unguarded() at /home/vagrant/php/housing/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:133
Illuminate\Database\Eloquent\FactoryBuilder->makeInstance() at /home/vagrant/php/housing/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:105
Illuminate\Database\Eloquent\FactoryBuilder->make() at /home/vagrant/php/housing/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:83
Illuminate\Database\Eloquent\FactoryBuilder->create() at /home/vagrant/php/housing/database/seeds/UsersTableSeeder.php:24
UsersTableSeeder->run() at /home/vagrant/php/housing/vendor/laravel/framework/src/Illuminate/Database/Seeder.php:42
Clearly, there is something wrong with dependency injection, but I don't know what. I know, that in this case I could just manually create instances of classes I need, but I want to know, how to do it properly. Can anyone help?

If you take a look at the documention of faker # https://github.com/fzaninotto/Faker#localization, you'll see that you can simply assign the proper localization as a parameter to create.
In your case, just use:
Faker\Factory::create('it_IT');
You don't need to add more parameters in the anonymous function when you define the factory.
Edit:
Just to add on the issue on dependency injection. If you trace the source code, it does not do any dependency injection underneath.
$factory->define(...)
Only sets an array of definitions
public function define($class, callable $attributes, $name = 'default')
{
$this->definitions[$class][$name] = $attributes;
}
Calling
Faker\Factory::create();
or
factory(App\User::class)->create();
$factory->of($class)
calls "of" method that instantiate FactoryBuilder
(see lines 169-172 of Illuminate\Database\Eloquent\Factory.php)
public function of($class, $name = 'default')
{
return new FactoryBuilder($class, $name, $this->definitions, $this->faker);
}
after that, it chains "create" method of FactoryBuilder that calls "make" method which also calls "makeInstance"
protected function makeInstance(array $attributes = [])
{
return Model::unguarded(function () use ($attributes) {
if (! isset($this->definitions[$this->class][$this->name])) {
throw new InvalidArgumentException("Unable to locate factory with name [{$this->name}].");
}
$definition = call_user_func($this->definitions[$this->class][$this->name], $this->faker, $attributes);
return new $this->class(array_merge($definition, $attributes));
});
}
Notice "call_user_func" inside "makeInstance", that is the one responsible for calling the anonymous function created as the 2nd argument to define (inside ModelFactory.php). It specifically pass only 2 arguments to the callable function, these are:
...$this->faker, $attributes);
Only 1 faker is passed on the first argument and an array of attributes on the 2nd argument (this is the one you saw on your ErrorException earlier)
That means you can only define your factory in this way:
$factory->define(App\User::class,
function (Faker\Generator $faker, $attributes=array()) {
return [
'name' => $faker->name,
'email' => $faker->email,
'password' => bcrypt(str_random(10)),
'remember_token' => str_random(10),
];
});
If you really need other classes, you can initialize it outside of "define" and use it in the function like this:
$sampleInstance = app(App\Sample::class);
$factory->define(App\User::class,
function (Faker\Generator $faker, $attributes=array()) use($sampleInstance){
//...do something here
//...or process the $attributes received
//...or call a method like
$sampleData = $sampleInstance->doSomething();
return [
'someField' => $sampleData,
'name' => $faker->name,
'email' => $faker->email,
'password' => bcrypt(str_random(10)),
'remember_token' => str_random(10),
];
});

You can put this setting in register() of AppServiceProvider:
$this->app->singleton(\Faker\Generator::class, function () {
return \Faker\Factory::create('it_IT');
});

Related

Ruby & fetching hash values magic

I'm trying to parse out JSON data and create my own dictionary to show a subset of the data. The thing is, I'm noticing that my input data changes based on what is scanned (with nmap). Some elements might be an array value, whereas some might not. The combinations seem to be pretty broad.
For instance, here is the simplest input where only an IP address was found:
{
'host' => {
'address' => {
'addr' => '192.168.0.1'
},
'status' => {...}
}
}
But then, the IP and MAC address might be found:
{
'host' => {
'address' => [{
'addrtype' => 'ipv4',
'addr' => '192.168.0.1',
},{
'addrtype' => 'mac',
'mac' => '00:AA:BB:CC:DD:EE',
},
'status' => {...}
}]
}
Those are just a couple examples. Other variations I've seen:
`host.class` = Array
`address.class` = Hash
`host['status'].class` = Array
etc...
As I go through to parse the output, I am first checking if the element is an Array, if it is, I access the key/values one way, whereas if it's not an array, I essentially have to duplicate my code with a few tweaks to it, which doesn't seem very eloquent:
hash = {}
if hosts.class == Array
hosts.each do |host|
ip = if host['address'].class == Array
host['address'][0]['addr']
else
host['address']['addr']
end
hash[ip] = {}
end
else
ip = if hosts['address'].class == Array
hosts['address'][0]['addr']
else
hosts['address']['addr']
end
hash[ip] = {}
end
puts hash
end
In the end, I'm just trying to find a better/eloquent way to produce a hash like below, while accounts for the possibility that an element may/may not be an Array:
{
'192.168.0.1' => {
'mac' => '00:aa:bb:cc:dd:ee',
'vendor' => 'Apple',
'ports' => {
'80' => {
'status' => 'open',
'service' => 'httpd'
}
'443' => {
'status' => 'filtered',
'service' => 'httpd'
}
}
},
192.168.0.2 => {
...
}
}
If there a ruby method that I haven't run across yet that will make this more fluid?
Not really... but you can make it always an array eg by doing something like:
hosts = [hosts] unless hosts.is_a?(Array)
or similar... then just pass that to your now-non-duplicated code. :)
The 20 lines of code in your question can be reduced to a single line using Array#wrap instead of conditionals, and using Enumerable#map instead of Enumerable#each:
Array.wrap(hosts).map { |host| [Array.wrap(host['address']).first['addr'], {}] }.to_h
Now that's magic!

How to bind elements of a MultiCheckbox to an Object in ZF2?

I'm using nested Zend\Form\Fieldsets and Zend\Form\Collections, that provide an extremely comfortable way to map complex object structures to the form, in order to get a complete object (ready to be saved) from the form input.
The element I want to add now to my form should represent a list of possible protocols. In the database it's a simple table with columns id and name and the objects' structure can be described like Endpoint has Protocol[]. I defined a MultiCheckbox (s. below), but I have no idea, how to bind this element to a Protocol prototype. For a Fieldset it works via Fieldset\setObject(...).
How to get the form processing mechanism of Zend\Form creating objects from checkboxes?
Code so far:
EndpointFieldset.php
// namespace ...;
// use ....;
class EndpointFieldset extends Fieldset
{
// ...
public function init()
{
parent::init();
$this->add(
[
'type' => 'multi_checkbox',
'name' => 'protocols',
'options' => [
'label' => _('protocols'),
'label_attributes' => [
'class' => 'col-md-1 protocol-field'
],
'value_options' => $this->getValueOptions(),
'selected' => static::PROTOCOLS_DUMMY_VALUE
]
]
);
}
// ...
protected function getValueOptions()
{
$valueOptions = [];
foreach (Protocol::PROTOCOLS as $key => $value) {
$valueOptions[] = [
'value' => $key,
'label' => $value
];
}
return $valueOptions;
}
}
myform.phml
use Zend\Form\View\Helper\FormMultiCheckbox;
echo $this->formMultiCheckbox($myFieldset->get('protocols'), FormMultiCheckbox::LABEL_PREPEND);
UPDATE
I found a workaround for the saving of a new entry: I simply complete the object provided by the form manually and make Protocol objects from the MultiCheckBox values. But when I pass complete object to the update form (in order to edit an existing entry), I get a notice and the checkboxes don't get built:
Notice: Object of class My\DataObject\Protocol could not be converted to int in /var/www/path/to/project/vendor/zendframework/zend-form/src/View/Helper/FormMultiCheckbox.php on line 202
My interpretation of this is, that the MultiCheckBox expects an array with values as primitive types (e.g. int). Instead it gets an array with Protocol objects and tries to use its values for in_array(...) -- and that cannot work.

How to decorate Elements of a Fieldset in a Collection in ZF2?

In my current project I'm using nested Zend\Form\Fieldsets and Zend\Form\Collections, that provide an extremely comfortable way to map complex object structures to the form, in order to get a complete object (ready to be saved) from the form input.
To the problem: I have a Fieldset FooFieldset containing an Element foo_element with a Label "foo element" (code see below) and need to use this twice: 1. as a single Fieldset; 2. in a Collection. At the first place in the form I want its elements to be displayed; at the the second place I want to disable the labels (or maybe change them). (I also want to format it another way in the second case, but the most important thing now is the label.)
How to decorate Zend\Form\Elements of a Zend\Form\Fieldset in a Zend\Form\Element\Collection depending on the context?
Code
class FooFieldset extends Fieldset implements InputFilterProviderInterface
{
public function init()
{
$this->add([
'type' => 'text',
'name' => foo_element',
'options' => ['label' => _('foo element')]
]);
}
public function getInputFilterSpecification() { ... }
}
class BarFieldset extends Fieldset implements InputFilterProviderInterface
{
public function init()
{
$this->add([
'name' => 'foo',
'type' => 'My\Form\Fieldset\Foo',
'options' => []
]);
}
public function getInputFilterSpecification() { ... }
}
class BuzFieldset extends Fieldset implements InputFilterProviderInterface
{
$this->add(
[
'name' => 'foos',
'type' => 'Zend\Form\Element\Collection',
'options' => [
'label' => _('multiple foos'),
'count' => 5,
'should_create_template' => true,
'template_placeholder' => '__placeholder__',
'allow_add' => true,
'target_element' => [
'type' => 'Order\Form\Fieldset\Foo',
],
'label_attributes' => [
'class' => 'col-md-12'
]
]
]);
public function getInputFilterSpecification() { ... }
}
echo $this->formRow($myForm->get('main_fieldset')->get('bar')->get('foo')->get('foo_element');
echo $this->formRow($myForm->get('main_fieldset')->get('buz')->get('foos');
Workaround 1
It would be possible to use another Fieldset, e.g. a sub-class of FooFieldst (sometnig like FooFieldsetForUsingInCollection extends FooFieldst) and adjust the Label (and other settings) there.
Workaround 2
It also would be possible to access the Collection's Elements in the view script and manipulate them there (as here demonstrated). But I don't really like this solution, since then the Fieldset is defined at multiple places. And it also need further effort, if the number of the Collection elements is variable.
It seems like you need to reuse the 'foos' collection and the 'bar' element together in their own fieldset while keeping how it is currently created.
I would
Move the collection element foo out of the BuzFieldset::init and into it's own factory (create the element and all it's options in the factory).
Register it with the form element manager as and new service, lets call it FooCollection. This element is now reusable and can be called from the form element manager as $fem->get('FooCollection').
Replace removed $fieldset->add('type' => 'Zend\Form\Element\Collection') with $fieldset->add('type' => 'FooCollection') in BuzFieldset.
Repeat for foo_element with a new service name of FooElement.
Then you need to create a new fieldset factory called FooCollectionAndFooElementFieldsetFactory this factory will return a new fieldset with both the FooCollection and FooElement attached.
In the factory of main_fieldset decide if you need to attach FooCollectionAndFooElementFieldsetFactory or the existing bar or baz fieldsets.

Basic use of TranslationServiceProvider in Silex

I'm trying to use the Silex TranslationServiceProvider in the most straighforward way i.e.
<?php
// web/index.php
require_once __DIR__.'/../vendor/autoload.php';
$app = new Silex\Application();
$app->register(new Silex\Provider\TranslationServiceProvider(), array(
'locale' => 'fr',
'locale_fallbacks' => array('en')
));
$app['translator.domains'] = array(
'messages' => array(
'en' => array('message_1' => 'Hello!'),
'fr' => array('message_1' => 'Bonjour')
));
echo $app['translator']->trans('message_1');
// I get 'Hello!' (why ?)
It seems that the 'locale' => 'fr' line when initializing the TranslationServiceProvider is not taken into account and that the only parameter that counts is locale_fallbacks (when I change locale_fallbacks to 'fr', the message is displayed in french)
Is there something very simple I am missing here ?
Thanks in advance
Edit
When I use the the setLocale function, it still doesn't work and seems to override the locale_fallbacks:
$app->register(new Silex\Provider\TranslationServiceProvider(), array(
'locale_fallbacks' => array('en')
));
$app['translator']->setLocale('fr');
echo $app['translator']->getLocale(); // returns 'fr' as expected
$app['translator.domains'] = array(
'messages' => array(
'en' => array('message_1' => 'Hello!'),
'fr' => array('message_1' => 'Bonjour')
));
echo $app['translator']->trans('message_1');
// now returns 'message_1' (??)
What's wrong with the way I use the provider ?
You must set the locale, otherwise the fallback is used:
$app['translator']->setLocale('fr');
I'm setting the locale in a $app->before() handler:
$app->before(function(Request $request) use ($app) {
// default language
$locale = 'en';
// quick and dirty ... try to detect the favorised language - to be improved!
if (!is_null($request->server->get('HTTP_ACCEPT_LANGUAGE'))) {
$langs = array();
// break up string into pieces (languages and q factors)
preg_match_all('/([a-z]{1,8}(-[a-z]{1,8})?)\s*(;\s*q\s*=\s*(1|0\.[0-9]+))?/i',
$request->server->get('HTTP_ACCEPT_LANGUAGE'), $lang_parse);
if (count($lang_parse[1]) > 0) {
foreach ($lang_parse[1] as $lang) {
if (false === (strpos($lang, '-'))) {
// only the country sign like 'de'
$locale = strtolower($lang);
} else {
// perhaps something like 'de-DE'
$locale = strtolower(substr($lang, 0, strpos($lang, '-')));
}
break;
}
}
$app['translator']->setLocale($locale);
$app['monolog']->addDebug('Set locale to '.$locale);
}
});
Two remarks on Ralf’s answer:
Almost all that is done in the before() middleware in unnecessary, as the Request class provides a convenience method for determining the “best” language based on the “Accept-Language” header. So basically, nothing more than this is required:
$app->before(
function (Request $request) use ($app) {
$app['translator']->setLocale($request->getPreferredLanguage(['en', 'fr']));
}
);
Silex uses the “magic” variable “{_locale}” in a route’s definition to set the locale accordingly. This means you do neither need to declare the locale when instantiating TranslationServiceProvider, nor call setLocale(), but simply declare the route like this:
$app->get('/{_locale}/path', ...);
Now, Twig and $app['translator'] (inside the Closure or controller method) will automatically be set to the correct locale.

Dynamically set SELECT attribute in zend2

What I am actually doing is, fetching a list of companies from the database and passing that to the form SELECT element.
So I created a Model file, which returns an array
//=== return an array of $ID => $name of companies to use in dropdown in reports form
public function getTotalResult($table, $type, $id) {
$this->table = $table;
$select = new Select();
$spec = new Where();
$spec->equalTo('status', 1);
if ($type == 'name') {
$spec->equalTo('id', $id);
}
$select->from($this->table);
$select->where($spec);
$resultSet = $this->selectWith($select);
//$resultSet->buffer();
return $resultSet;
}
public function resultList($table){
$results = $this->getTotalResult($table, '', '');
foreach ($results as $result) {
$this->id[] = $result->id;
$this->name[] = $result->name;
}
$result = array_combine($this->id, $this->name);
return $result;
}
Then I tested this in my Controller, which returned exactly what I wanted:
use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use SpangelLogin\Model\Register; // <-- Add this import
use SpangelLogin\Model\companyList; // <-- Add this import
class RegisterController extends AbstractActionController
{
protected $registerTable;
protected $companyList;
public function getcompanyList()
{
if (!$this->companyList) {
$sm = $this->getServiceLocator();
$this->companyList = $sm->get('SpangelLogin\Model\companyList');
}
return $this->companyList;
}
public function indexAction()
{
//== get list of companies
$company_table = 'rs_company';
$sector_table = 'rs_sector';
$companiesList = $this->getcompanyList()->getName($company_table, 2);
}
}
So now I want this companiesList array passed in my form's Select Element. How can I achieve that. Here is my form in which I am using select.
use Zend\Form\Form;
use Zend\Form\Element;
class SectorReportForm extends Form
{
public function __construct($name = null)
{
// we want to ignore the name passed
parent::__construct('sectorreport');
$companiesArray = $this->companiesList();
$sectorsArray = $this->sectorsList();
$this->setAttribute('method', 'post');
$this->setAttribute('enctype','multipart/form-data');
$this->add(array(
'type' => 'Zend\Form\Element\Select',
'name' => 'company',
'attributes' => array(
'id' => 'company',
'multiple' => true,
'options' => $companiesArray,
),
'options' => array(
'label' => 'Company',
),
));
$this->add(array(
'name' => 'submit',
'attributes' => array(
'type' => 'submit',
'value' => 'Upload',
'id' => 'submitbutton',
'class' => 'button violet right'
),
));
}
}
From a Design-Perspective, the best approach would be to handle this via Dependency-Injection. That sneaky little buzzword that confuses people so much, but actually is nothing more but to forward data between objects :P
General Dependency-Injection for Forms can be seen looking at the following answer, as well as my Blog article
How to get data from different model for select?
Zend\Form\Element\Select and Database-Values
If you do not want to go this approach, you can handle this at the Controller level, too.
$form = new My\Form();
$select = $form->get('selectCountries');
$model = new My\Countries();
$listData = $model->getCountriesAsArray();
$select->setValueOptions($listData);
I still advise you to go the different approach ;) Keeps the controllers more clean, too, which is always a good thing. Separation of concern!

Resources