Can I have a ZF3 MVC Framework Controller with parameters in the constructor? - zend-framework-mvc

I have a Zend Framework 3 MVC app with a controller with two end points. Both need to access the same class. It seems like the best way to do this would be to would be to make an instance of this class a property of the controller class like this:
class IndexController extends AbstractActionController
{
/**
* var Utility $utility
*/
protected $utility;
public function __construct(Utility $utility)
{
$this->utility = $utility;
}
public function indexAction()
{
$this->utility->doA('param1');
return new ViewModel();
}
public function otherAction()
{
$results = $this->utility->validateRequest($this->request);
if ($results)
{
return new ViewModel();
}
else
{
throw new Exception('Invalid request');
}
}
}
However, I don't know how to pass paramaters to the constructor since I don't know where Zend Framework "makes" it.

Zend Framework uses a concept called Dependency Injection. This is based on the D in SOLID, dependency inversion. Theory aside, you need to make a custom factory for your controller in modules.config.php. You also need to make a factory for the class calld Utility.
So first of all, you probably made your project with a command similar to composer create-project -sdev zendframework/skeleton-application. If you did that you probably don't have the latest version of Service Manager. See if the file vendor/bin/generate-factory-for-class exists. If not, execute composer update zendframework/zend-servicemanager to add it there.
Now lets make a factory for the utility class. Lets assume its in module/Application/src/Service/Utility.php and has the namespace Application\Service. You just type vendor/bin/generate-factory-for-class Application\\Service\\Utility > module/Application/src/Service/UtilityFactory.php. If you look in that file you can see:
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new Utility();
}
Now lets do the same for the controller with vendor/bin/generate-factory-for-class Application\\Controller\\IndexController > module/Application/src/Controller/IndexControllerFactory.php. Open this factory and see its a little more complex.
public function __invoke(ContainerInterface $container, $requestedName, array $options = null)
{
return new IndexController($container->get(\Application\Service\Utility::class));
}
$container is your dependency injection container. It executes the __invoke command in these factories when called.
One more thing left to do. you need to edit your module.config.php. Replace the line
'controllers' => [
'factories' => [
Controller\IndexController::class => InvokableFactory::class,
],
],
with
'controllers' => [
'factories' => [
Controller\IndexController::class => Controller\IndexControllerFactory::class,
],
],
Now add the following section to the config:
'service_manager' => [
'factories' => [
Service\Utility::class => InvokableFactory::class,
],
],
Then your controller should work.

Related

Cannot generate HalResource for object of type ArrayObject

I've some problems to return a paginator object as HAL json collection. I'm using the latest versions of zend-expressive and zend-expressive-hal.
This is the setting from my ConfigProvider:
public function __invoke() : array
{
return [
'dependencies' => $this->getDependencies(),
MetadataMap::class => $this->getHalConfig(),
];
}
public function getHalConfig() : array
{
return [
[
'__class__' => RouteBasedCollectionMetadata::class,
'collection_class' => RoleCollection::class,
'collection_relation' => 'user_roles',
'route' => 'api.user.roles',
],
];
}
And these are my handler methods:
public function get(ServerRequestInterface $request) : ResponseInterface
{
// read some records from the database
$select = new Select();
$select->from(['r' => 'user_roles']);
$select->columns(['id', 'name']);
$paginator = new RoleCollection(new DbSelect($select, $this->dbAdapter));
$paginator->setItemCountPerPage(25);
$paginator->setCurrentPageNumber(1);
return $this->createResponse($request, $paginator);
}
private function createResponse(ServerRequestInterface $request, $instance) : ResponseInterface
{
return $this->responseFactory->createResponse(
$request,
$this->resourceGenerator->fromObject($instance, $request)
);
}
The RoleCollection class is only an inheritance of the Paginator:
class RoleCollection extends Paginator
{
}
The error message which I get is:
Cannot generate Zend\Expressive\Hal\HalResource for object of type ArrayObject; not in metadata map
I think you are missing the metadata for the Role object itself.
For example this is something similar for my posts object:
MetadataMap::class => [
[
'__class__' => RouteBasedCollectionMetadata::class,
'collection_class' => Posts::class,
'collection_relation' => 'posts',
'route' => 'api.posts',
],
[
'__class__' => RouteBasedResourceMetadata::class,
'resource_class' => Post::class,
'route' => 'api.posts.view',
'extractor' => ArraySerializable::class,
],
],
You have only described the collection and the resource class is missing for a single role.
I also see the resource generator tries to parse an ArrayObject. This should be wrapped in a Role object, which you can add to the MetadataMap.
Where it goes wrong in your code is this line:
$paginator = new RoleCollection(new DbSelect($select, $this->dbAdapter));
This adds the result of a query into the paginator, but the paginator does not know how to handle it. If I remember correctly, the DbSelect return a ResultSet. I'm guessing this is where the ArrayObject is coming from. What you probably need is to override that ResultSet and make sure it returns an array of Role objects. You might want to look into the dbselect adapter and the hydrating resultset.
Once you have the Role object in the paginator, you can describe it in the metadata.
[
'__class__' => RouteBasedResourceMetadata::class,
'resource_class' => UserRole::class,
'route' => 'api.roles',
'extractor' => ...,
],
I use doctrine myself with hal so zend-db is out of my scope. If you need more help, I suggest the zf forums.

I can't get the data in appends with json in Laravel

I have two models in laravel project Item and ItemImgs
Item.php
class Item extends Model
{
protected $appends = [
'photo',
];
public function imgs()
{
return $this->hasMany(ItemImage::class);
}
public function getPhotoAttribute()
{
$img = $this->imgs->first();
return $img.src;
}
}
it's worked in views
dd(Item::all()); //worked
{{ $cane->photo}}; //worked
but when I try to get json
return response()->json([
'items' => Item::with('imgs')->get(),
]);
// not worked. Got timeout 500
You cannot use dot notation in PHP.
public function getPhotoAttribute()
{
$img = $this->imgs->first();
return $img.src; // Dot notation is not allowed
}
but you've to use:
public function getPhotoAttribute()
{
$img = $this->imgs->first();
return $img->src;
}
if what you're trying to do is to get the items that have imgs() then what you should do is query by relationship existence, as mentioned in the docs
https://laravel.com/docs/5.8/eloquent-relationships#querying-relationship-existence
'items' => Item::has('imgs')->get()
It is not possible to refer to the linked model tables in attributes. It works in views but gives out a memory error when outputting an array through json.
public function getPhotoAttribute(){
$img = ItemImage::where('item', $this->id)-
>first();
}
It works that way, but it's not elegant.

Yii2 creating a custom function in the model using Gii model generator

I am working on Yii2 using Gii to generate models. What I am trying to do is to customize my models such that all of them will have the following function
public static function getFoobarList()
{
$models = Foobar::find()->all();
return ArrayHelper::map($models, 'id', 'foobar');
}
Where Foobar is the name of individual models.
Thank you in advance.
You can create a custom template for your models which gii can use to generate your class.
Something like the following, added to the top of a copy of the file /vendor/yiisoft/yii2-gii/generators/model/default/model.php and the new file stored in, for example, #app/myTemplates/model/default
/**
* your doc string
*/
public static function get<?php echo $className; ?>List()
{
$models = static::find()->all();
return ArrayHelper::map($models, 'id', static::tableName());
}
will add the method you're looking for to any model created with the new template.
In your config something like
// config/web.php for basic app
// ...
if (YII_ENV_DEV) {
$config['modules']['gii'] = [
'class' => 'yii\gii\Module',
'allowedIPs' => ['127.0.0.1', '::1', '192.168.0.*', '192.168.178.20'],
'generators' => [ //here
'model' => [ // generator name
'class' => 'yii\gii\generators\model\Generator', // generator class
'templates' => [ //setting for out templates
'myModel' => '#app/myTemplates/model/default', // template name => path to template
]
]
],
];
}
will allow you to select your custom template when using gii, from the 'Code Template' menu.
Since you want this in all the models, another solution would be to add this function in ActiveRecord Model from which all generated models extend. You just need to change the function a bit to perform the required functionality.
Just add this to your ActiveRecord class:
public static function getModelList()
{
$models = static::find()->all();
return ArrayHelper::map($models, 'id', static::tableName());
}
To use this for any model, example Foobar all you'll need to do is:
Foobar::getModelList();

Add new attribute dynamically to the existing model object in Yii2 framework

In Yii2 framework is it possible to add a new attribute dynamically to an existing object, which is retrieved from Database?
Example
//Retrieve from $result
$result = Result::findone(1);
//Add dynamic attribute to the object say 'result'
$result->attributes = array('attempt' => 1);
If it is not possible, please suggest an alternate best method to implement it.
Finally I would be converting the result to a json object. In my application, at the behaviour code block, I have used like this:
'formats' => [
'application/json' => Response::FORMAT_JSON,
],
You can add define a public variable inside your model, that will store dynamic attributes as associative array. It'll look something like this:
class Result extends \yii\db\ActiveRecord implements Arrayable
{
public $dynamic;
// Implementation of Arrayable fields() method, for JSON
public function fields()
{
return [
'id' => 'id',
'created_at' => 'created_at',
// other attributes...
'dynamic' => 'dynamic',
];
}
...
..in your action pass some dynamic values to your model, and return everything as JSON:
public function actionJson()
{
\Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
$model = Result::findOne(1);
$model->dynamic = [
'field1' => 'value1',
'field2' => 2,
'field3' => 3.33,
];
return $model;
}
In result you will get JSON like this:
{"id":1,"created_at":1499497557,"dynamic":{"field1":"value1","field2":2,"field3":3.33}}

How to update customer in braintree payment method

I have integrated braintree method in yii2 rest api Using this reference.. I want to update the customer but I am getting following error:
Missing argument 2 for Braintree\Customer::update()
Below is my code :
$braintree = Yii::$app->braintree;
$response = $braintree->call('Customer', 'update','15552090',[
'firstName' => 'test-1545',
'lastName' => 'asdf',
'company' => 'New Company',
'email' => 'new.email#example.com',
'phone' => 'new phone',
'fax' => 'new fax',
'website' => 'http://new.example.com'
]);
print_r($response); die;
I am stack here how to pass the arguments?
It's a problem of this particular extension. See this issue on Github.
Issue OP recommends this fix:
public function call($command, $method, $values, $values2 = null)
{
$class = strtr("{class}_{command}", [
'{class}' => $this->_prefix,
'{command}' => $command,
));
if ($values2) {
return call_user_func(array($class, $method), $values, $values2);
else {
return call_user_func(array($class, $method), $values);
}
}
while extension author recommends this:
if (is_array($values)) {
call_user_func_array(...);
} else {
call_user_func(...);
}
Either way you need to override this component with your own and apply a patch.
Note that the amount of code in application is small (64 lines in one file) so you can create your own wrapper or find better one because this issue is still not fixed.
And maybe is better to directly use braintree_php methods which will be more clear than magical call.
Update: To override component, create own class extending from bryglen's, place it for example in common/components folder in case of using advanced app.
namespace common\components;
class Braintree extends \bryglen\braintree\Braintree
{
public function call($command, $method, $values)
{
// Override logic here
}
}
Then replace extension class name with your custom one in config:
'components' => [
'braintree' => [
'class' => 'common\components\Braintree',
'environment' => 'sandbox',
'merchantId' => 'your_merchant_id',
'publicKey' => 'your_public_key',
'privateKey' => 'your_private_key',
],
],