Laravel Nova show computed field in BelongsToMany sub table - laravel-nova

Using Laravel Nova, I want to show a computed field in a BelongsToMany subview. In the intermediate model I have setup a virtual attribute field which computes a value using connected tables. However this field is not getting shown probably because Nova checks the withPivot fields (to which it cannot be added as it's not a real field). Is there an alternative method to get this to work?
The database model behind this is:
[GameVariation] has BelongsToMany using [GameVariationMatch] with [GameMatch]
[GameMatch] HasOne [Match]
In the GameVariation resource I've setup a BelongsToMany field which should display the computed field:
BelongsToMany::make( 'Game Matches', 'game_matches', GameMatch::class )
->fields( function () {
return [
Text::make( 'Match Data' )->displayUsing(function() {
return $this->match_data;
})
];
} ),
In the GameVariation model the tables are connected with BelongsToMany:
final public function game_matches(): BelongsToMany {
return $this->belongsToMany(
GameMatch::class
)
->using( GameVariationMatch::class );
}
In the pivot tabel model GameVariationMatch the computed field is setup like this:
final public function getMatchDataAttribute(): string {
return $this
->game_match
->match
->match_data;
}

As it turns out, on the GameVariation resource the index view of the BelongsToMany to GameMatch is served from the GameMatch resource. When only setting up ->fields() on the GameVariation BelongsToMany field, it won't show up. The recommended way is to set up ->fields() using a field class like this:
<?php
namespace App\Nova\Fields;
use App\Models\GameVariationMatch;
use Illuminate\Http\Request;
use Laravel\Nova\Fields\Text;
class GameVariationMatchFields {
/**
* Get the pivot fields for the relationship.
*
* #param Request $request
*
* #return array
*/
final public function __invoke( Request $request ): array {
return [
Text::make( 'Match Data', function ( GameVariationMatch $GameVariationMatch ) {
return $GameVariationMatch->match_data;
} ),
];
}
}
In the above, the computed Text field receives the intermediate pivot model and can therefore access all computed attributes.
The field class is used in the GameVariation resource as:
BelongsToMany::make( 'Game Matches', 'game_matches', GameMatch::class )
->fields( new RoundMatchVariationFields ),
And in the GameMatch resource as:
BelongsToMany::make( 'Game Variations', 'game_variations', GameVariation::class )
->fields( new GameVariationMatchFields ),
As described in official Nova docs on pivot fields https://nova.laravel.com/docs/2.0/resources/relationships.html#belongstomany

Related

How to getData() from a Zend\Form before the validation in Zend Framework 2/3?

I have a complex nested (order) Zend\Form, that can be edited multiple times. The user first creates an order, but doesn't need to place it immediately. He can just save the order (or more exact: its data) and edit it later. In this case the application loads an Order object (with all its nested structure) and binds it to the form. The important steps are:
get ID of the order from the request
get the Order object by ID
$orderForm->bind($orderObject)
...
Now I want to catch the data and serialize it to JSON. (The background: Forms cloning -- in the next step a empty new form should created and the should be passed to it; after saving we'll get a clone.) It should happen between 2 and 3. So I'm trying
$formData = $this->orderForm->getData();
$formJson = json_encode($formData, JSON_UNESCAPED_SLASHES);
and getting the error:
Zend\Form\Form::getData cannot return data as validation has not yet occurred
Well, I could try to work around it and validate the form:
$formIsValid = $this->orderForm->isValid();
but it only leads to further troubles:
Zend\InputFilter\BaseInputFilter::setData expects an array or Traversable argument; received NULL
Is there a way to get the form data before the validation?
Okay, the comment space is way too small to say everything about what you try to archive. Let 's refactor every single step you mentioned in the starting post. This will lead us to your goal. It 's all about hydration.
This will be a small example, how an order entity with products in it could look like. After the order entity follows the product entity, which we need for this example.
namespace Application\Entity;
class Order implements \JsonSerializable
{
/**
* ID of the order
* #var integer
*/
protected $orderID;
/**
* Array of \Application\Entity\Product
* #var array
*/
protected $products;
public function getOrderID() : integer
{
return $this->orderID;
}
public function setOrderID(integer $orderID) : Order
{
$this->orderID = $orderID;
return $this;
}
public function getProducts()
{
if ($this->products == null) {
$this->products = [];
}
return $this->products;
}
public function setProducts(array $products) : Order
{
$this->products = $products;
return $this;
}
/**
* #see \JsonSerializable::jsonSerialize()
*/
public function jsonSerialize()
{
return get_object_vars($this);
}
}
The following entity represents a product.
class Product implements \JsonSerializable
{
protected $productID;
protected $name;
public function getProductID() : integer
{
return $this->productID;
}
public function setProductID(integer $productID) : Product
{
$this->productID = $productID;
return $this;
}
public function getName() : string
{
return $this->name;
}
public function setName(string $name) : Product
{
$this->name = $name;
return $this;
}
/**
* #see \JsonSerializable::jsonSerialize()
*/
public function jsonSerialize()
{
return get_object_vars($this);
}
}
Above you see our entity, wich represents a single order with several possible products in it. The second member products can be an array with Product entities. This entity represents the data structure of our simple order.
At this point we need a form, which uses this entites as objects for the data it contains. A possible factory for our form could look like this.
namespace Application\Form\Factory;
class OrderFormFactory implements FactoryInterface
{
public function createService(ServiceLocatorInterface $serviceLocator)
{
$parentLocator = $serviceLocator->getServiceLocator();
$inputFilter = $parentLocator->get('InputFilterManager')->get(OrderInputFiler::class);
$hydrator = new ClassMethods(false);
$entity = new OrderEntity();
return (new OrderForm())
->setInputFilter($inputFilter)
->setHydrator($hydrator)
->setObject($entity);
}
}
This is the factory for our form. We set a hydrator, an input filter and an entity for the form. So you don 't have to bind something. The following code shows, how to handle data with this form.
// retrieve an order from database by id
// This returns a order entity as result
$order = $this->getServiceLocator()->get(OrderTableGateway::class)->fetchById($id);
// Extract the order data from object to array assumed the
// retrieved data from data base is an OrderEntity object
// the hydrator will use the get* methods of the entity and returns an array
$data = (new ClassMethods(false))->extract($order);
// fill the form with the extracted data
$form = $this->getServiceLocator()->get('FormElementManager')->get(OrderForm::class);
$form->setData($data);
if ($form->isValid()) {
// returns a validated order entity
$order = $form->getData();
}
It is absolutely not possible to get data from a form, that is not validated yet. You have to validate the form data and after that you can get the filtered / validated data from the form. Hydrators and entities will help you a lot when you have to handle a lot of data.

laravel updateOrCreate() with dynamic table

I'm working with project where tables are created all the time depending on user adding new properties, so for my model i have below
class Hotel extends Model
{
public function __construct($hotel)
{
$this->table = $hotel;
}
protected $fillable = ['date', 'sold', 'sold_diff', 'rev', 'rev_diff', 'row', 'date_col', 'sold_col', 'rev_col'];
}
and i can use the table in controller by doing
$hotel_table = new Hotel($table);
but i like to use Model::updateOrCreate() when I'm adding or updating rows in table, and I'm not sure how to do that.
This is the signature for the updateOrCreate function
"static Model updateOrCreate( array $attributes, array $values = array())"
For you to update or create, you can pass the condition that must be met to update the table to the first argument and the values to be updated to the second.
for example.
$primaryKey = isset($request->input('id')) ? $request->input('id') : null;
$myModel = Model::updateOrCreate(['id' => $primaryKey], $request->all());
so with this if id is in the request object the table will be updated but if not a new record will be created.
In Laravel 5.2 you can use simply like this
class Hotel extends Model
{
protect $table = 'hotels'
protected $fillable = ['date', 'sold', 'sold_diff', 'rev', 'rev_diff', 'row', 'date_col', 'sold_col', 'rev_col'];
// protected $guarded = []; you can use this instead of `$fillable` this is for all columns fillable
}
now in your controller you can use
Model::update();
Model::create();

Zend\Db Model with Child Models

ZF2 project - no Doctrine, using native Zend\Db: Have the following structure:
Controller
ProductController
Model
Product
ProductTable
ProductType
ProductTypeTable
Product is the model, has variables corresponding to the “products" table fields.
ProductTable is table class which is connected to the database via tableGateway. ProductTable has getItem() method to retrieve requested product by “id”.
ProductType is the model, has variables like id, name, description corresponding to the “productTypes" table fields.
ProductTypeTable is table class just like ProductTable.
Each product belongs to a certain ProductType
products.productTypeId = productTypes.id
is the relation.
In ProductTable->getItem() method, I can simply get productTypeId.
I can use joins to get productTypes.name, productTypes.description, or any field from "productTypes" table.
But I don’t want to do this - instead dealing with new variables in Product entity like productTypeName, productTypeDesc,
I’d like to have Product->getProductType() and set it to be a ProductType object, so I can get Product->getProductType() ->getName() to get product type name.
Simply I’d like to assign a child model as a variable of the parent model.
I can do this in the controller like below:
$product = $this->getProductTable()->getItem(7); // id = 7
$product->setProductType($this->getProductTypeTable()
->getItem($product->getProductTypeId());
But I’d like to make it happen in product table class getItem() method. So I don’t have to think about it in every controller, and it is kind of encapsulated.
What is the right way to do this?
Thank you.
The issue that you have is the Table Gateway pattern is only really any good at abstracting database access to a a single database table. It does not in anyway allow for the hydration of entities or management of relationships. Object Relationship Mappers (ORM's), such as Doctrine, solve this problem.
If Doctrine, for whatever reason, is inappropriate for your use case an alternative could be implementing the Data Mapper Pattern
The Data Mapper is a layer of software that separates the in-memory objects from the database. Its responsibility is to transfer data between the two and also to isolate them from each other
The data mapper will use the table gateway to fetch the required data for each table and construct the Product instance, including it's associated ProductType. You would then expose the mapper to the controller (rather than the table gateway).
A simple example of a ProductMapper.
class ProductMapper
{
// #var \Zend\Db\TableGateway\TableGateway
protected $productTable;
protected $productTypeMapper;
// an 'identity map' of loaded products
protected $loaded = [];
public function __construct(ProductTable $productTable, ProductTypeMapper $productTypeMapper)
{
$this->productTable = $productTable;
$this->productTypeMapper = $productTypeMapper;
}
protected function hydrate(Product $product, array $data)
{
$product->setId($data['id']);
$product->setName($data['name']);
$product->setFoo($data['foo']);
if (isset($data['type_id'])) {
// Load a fully constructed product type from the database
$type = $this->productTypeMapper->findById($data['type_id']);
$product->setType($type);
}
return $product;
}
public function findById($id)
{
if (isset($this->loaded[$id])) {
return $this->loaded[$id];
}
// Get the data
$row = $this->productTable->select(['id' => $id]);
if (empty($row)) {
throw new SomeCustomException("No product could be found with id $id");
}
// Create and hydrate the product
$product = $this->hydrate(new Product, $row->current())
$this->loaded[$id] = $product;
return $product;
}
public function save(array $data);
public function update($data);
public function delete($id);
}
You can achieve this, you just have to follow the following 3 steps:
Make your Product->exchangeArray() function smarter
Get all required ProductType fields, using a prefix helps for example: type_
Add #var ProductType so you will have proper autocompete (works for me in Eclipse)
<?php
namespace Product\Model\Product;
class Product {
public $id;
...
/**
* #var ProductType
*/
public $productType;
...
public function exchangeArray( $data ) {
$this->id = (isset($data['id'])) ? $data['id'] : null;
...
$productType = new ProductType();
$typeData = array(
'id' => $data['type_id'],
'value' => $data['type_value']
);
$productType->exchangeArray( $typeData );
$this->productType = $productType;
}
}

how to set value from database in Zend Framework 2 in add form element

i new to zenframework 2. i have correctly set up zendframework 2,doctrine and zfcUser.All work correctly.
my issue now is now regarding how to prepoulated a form if a member is already logged in.
this is where i extend zfcUser to obtain the Id of a loggged in member:
public function setid( $id)
{
$this->id = $id;
}
public function getId()
{
if (!$this->id) {
$this->setid($this->zfcUserAuthentication()->getAuthService()->getIdentity()->getId());
}
return $this->id;
}
i know want to use that Id to obtain the values from the database and then populate the form with those values.
this is my form:
public function aboutYouAction()
{
$id = $this->getId() ;
$form = new CreateAboutYouForm($this->getEntityManager());
$aboutYou = new AboutYou();
$form->setInputFilter($aboutYou->getInputFilter());
$form->bind($aboutYou);
if ($this->request->isPost())
{
$form->setData($this->request->getPost());
if ($form->isValid())
{
$post = $this->request->getPost();
$this->getEntityManager()->persist($aboutYou);
$this->getEntityManager()->flush();
return $this->redirect()->toRoute('worker', array('action' => 'aboutYou'));
}
}
$messages='';
// return array('form' => $form);
return new ViewModel(array('form' => $form, 'messages' => $messages));
}
To set the values on the form all you need to do is $form->bind($aboutYou)
The bind() method is designed to take the passed entity instance and map it to the forms elements; This process being referred to as form hydration.
Depending on the hydrator attached to the form or fieldset (With doctrine this would normally be the DoctrineModule\Stdlib\Hydrator\DoctrineObject) this should be able to evaluate the AboutYou fields, including any entity references/associations, and set the corresponding form elements values. I'm assuming that one of these fields is user.
In you specific case it seems you are binding a new entity (which therefore will not have any properties set, such as your user)
$aboutYou = new AboutYou(); // Brand new entity
$form->bind($aboutYou); // Binding to the form without any data
What this means is that the form is trying to set the values of the elements but the provided AboutYou class has no data to set (as its new and was not loaded via doctrine) and/or the properties of the AboutYou class to not correctly map to the form's elements.
If you wish to bind the user you will need to fetch the populated instance. This can be done using doctrine ($objectManager->find('AboutYou', $aboutYouId)) or if you need to set the current logged in user call the controller plugin ZfcUser\Controller\Plugin\ZfcUserAuthentication from within the controller and no where else.
You workflow should be similar to this (illustration purposes only)
// Controller
public function aboutYouAction()
{
// Get the id via posted/query/route params
$aboutYouId = $this->params('id', false);
// get the object manager
$objectManager = $this->getServiceLocator()->get('ObjectManager');
// Fetch the populated instance
$aboutYou = $objectManager->find('AboutYou', $aboutYouId);
// here the about you entity should be populated with a user object
// so that if you were to call $aboutYou->getUser() it would return an user object
// Get the form from the service manager (rather than creating it in the controller)
// meaning you should create a factory service for this
$form = $this->getServiceLocator()->get('MyForm');
// Bind the populated object to the form
$form->bind($aboutYou);
//... rest of the action such as handle edits etc
}

symfony admin filter with join

I have a table, heading, that has an import_profile_id. import_profile has a bank_id.
On my headings list page in my admin, I'd like to add the ability to filter by bank_id. However, since heading doesn't have a bank_id - it needs to go through import_profile to get that - I can't just add a bank_id field and expect it to work.
Can anyone explain how to do this? The closest thing I've found is this post but I don't think it really addresses my issue.
This can be done by using virtual columns like the post you found. The virtual column is a way to add a new criteria to filter using the autogenerated filter provided by symfony. It works like this:
1 - Go to the generator.yml of the admin module and add the name of the virtual column that will create and add
<!-- apps/backend/modules/module_name/config/generator.yml -->
filter:
[virtual_column_name, and, other, filter, columns]
2 - In your lib/filter/{TableName}FormFilter.class.php (I think in your case must be HeadingFormFilter) you have to define that virtual column in the configure() method
public function configure()
{
//Type of widget (could be sfWidgetFormChoice with bank names)
$this->widgetSchema['virtual_column_name'] = new sfWidgetFormInputText(array(
'label' => 'Virtual Column Label'
));
//Type of validator for filter
$this->validatorSchema['virtual_column_name'] = new sfValidatorPass(array ('required' => false));
}
3 - Override the getFields() of that class to define it in the filter and set the filter function
public function getFields()
{
$fields = parent::getFields();
//the right 'virtual_column_name' is the method to filter
$fields['virtual_column_name'] = 'virtual_column_name';
return $fields;
}
4 - Finally you have to define the filter method. This method must be named after the add...ColumnQuery pattern, in our case must be addVirtualColumnNameColumnQuery(not a happy name choice :P), so
public function addVirtualColumnNameColumnQuery($query, $field, $value)
{
//add your filter query!
//for example in your case
$rootAlias = $query->getRootAlias();
$query->innerJoin($rootAlias . '.ImportProfile ip')
->andWhere('ip.BankId = ?', $value);
//remember to return the $query!
return $query;
}
Done! You can know filter by bank_id.

Resources