Symfony 2 override entity field property - database

I want to override the entity field property. I need to get data from another database table (mapped by id). It should be a combination of "artikelnummer" and a field called "name" from another database table.
$builder->add('schlauch', 'entity', array(
'class' => 'SchlauchBundle:Artikelspezifikation',
'property' => 'artikelnummer',
'attr' => array(
'class' => 'extended-select'
),
'data_class' => null
));
The field "artikelnummer" outputs something like "12345" but I need to add the name (from another database table called "schlauch"), so it should look like "12345 Articlename". I tried a query in the entity file, but I dont't want to manipulate the output everywhere.
Is it possible to use a query for property and override it?

You can simple solve that by adding new getter to you entity:
class Artikelspezifikation
{
//…
/**
* #var Schlauch
*
* #ORM\ManyToOne(targetEntity="Schlauch", inversedBy="artikelspezifikations")
*/
private $schlauch;
//…
/**
* Get display name
*
* #return string
*/
public function getDisplayName()
{
return $this->artikelnummer . ' ' . $this->schlauch->getArtikelName();
}
//…
/**
* Set schlauch
*
* #param \SchlauchBundle\Entity\Schlauch $schlauch
*
* #return Artikelspezifikation
*/
public function setCategory(\SchlauchBundle\Entity\Schlauch $schlauch = null)
{
$this->schlauch = $schlauch;
return $this;
}
/**
* Get schlauch
*
* #return \SchlauchBundle\Entity\Schlauch
*/
public function getCategory()
{
return $this->schlauch;
}
}
And in your form class just change property:
$builder->add('schlauch', 'entity', array(
'class' => 'SchlauchBundle:Artikelspezifikation',
'property' => 'displayName',
'attr' => array(
'class' => 'extended-select'
),
'data_class' => null
));

Related

Could not view with composite pK CakePHP4

In cakephp-4.x I could not access Controller's view action for composite primary key table. (http://localhost:8765/invoice-items/view/1)
Here are samples of the code created by cake bake:
InvoiceItemsTable calss in which the primary key is defined as composite.
class InvoiceItemsTable extends Table
{
/**
* Initialize method
*
* #param array $config The configuration for the Table.
* #return void
*/
public function initialize(array $config): void
{
parent::initialize($config);
$this->setTable('invoice_items');
$this->setDisplayField(['item_id', 'invoice_id', 'unit_id']);
$this->setPrimaryKey(['item_id', 'invoice_id', 'unit_id']);
$this->addBehavior('Timestamp');
$this->belongsTo('Items', [
'foreignKey' => 'item_id',
'joinType' => 'INNER',
]);
$this->belongsTo('Invoices', [
'foreignKey' => 'invoice_id',
'joinType' => 'INNER',
]);
$this->belongsTo('Units', [
'foreignKey' => 'unit_id',
'joinType' => 'INNER',
]);
}
...
InvoiceItemsController view method:
/**
* View method
*
* #param string|null $id Invoice Item id.
* #return \Cake\Http\Response|null|void Renders view
* #throws \Cake\Datasource\Exception\RecordNotFoundException When record not found.
*/
public function view($id = null)
{
$invoiceItem = $this->InvoiceItems->get($id, [
'contain' => ['Items', 'Invoices', 'Units'],
]);
$this->set(compact('invoiceItem'));
}
Finally a screen shot of invoice_items table structure from phpmyadmin:
I have tried to access the view like (http://localhost:8765/invoice-items/view/1,1,6) but I got the same error ... with primary key ['1,1,6']. I do not know how to represent the composite primary key in the URL? or what is the problem?
I use CakePHP version 4.4.2
Table::get() expects composite keys to be passed as arrays, eg [1, 1, 6].
Assuming you're using the fallback routes from the default app skeleton, you can pass additional arguments as path parts, eg:
/invoice-items/view/1/1/6
and accept them in your controller action like:
public function view($itemId, $invoiceId, $unitId)
and build an array from the accordingly to pass to get() as the "id":
$this->InvoiceItems->get([$itemId, $invoiceId, $unitId], /* ... */)
In case you're using custom routes with fixed parameters, add additional ones in whatever form you like, for example with dashes:
$routes
->connect(
'/invoice-items/view/{itemId}-{invoiceId}-{unitId}',
['controller' => 'InvoiceItems', 'action' => 'view']
)
->setPass(['itemId', 'invoiceId', 'unitId'])
->setPatterns([
'itemId' => '\d+',
'invoiceId' => '\d+',
'unitId' => '\d+',
]);
then your URL would look like:
/invoice-items/view/1-1-6
See also
Cookbook > Routing > Route Elements
Cookbook > Routing > Passing Parameters to Action
Cookbook > Routing > Fallbacks Method

DebugKit Error: CakePHP get() mystery empty elements in associations

EDIT: This is after upgrading from CakePHP 4.0 to 4.3
With DebugKit turned on, CakePHP is throwing the following error message:
Argument 1 passed to Cake\ORM\Entity::get() must be of the type string, bool given, called in /Users/thomasbelknap/one-vision/vendor/cakephp/cakephp/src/Datasource/EntityTrait.php on line 607
This appears to be as a result of an empty, numerically-indexed element in the return array of a database query. Let me explain:
In my system, a User has many Estimates (and also Carts and Orders, but more on that). These two clauses in the User and Estimate table files bare this out:
$this->belongsTo('Users', [
'foreignKey' => 'user_id',
]);
::snip::
$this->hasMany('Estimates', [
'foreignKey' => 'user_id',
'dependent' => true,
]);
I am querying this information on a "Dashboard" page using the following query structure:
$user = $this->Users->get($this->getRequest()->getSession()->read('Auth.id'), [
'contain' => [
'UserGroups',
'Carts' => function ($q) {
return $q->limit(5);
},
'Orders' => function ($q) {
return $q->limit(5);
},
'Orders.OrderStatuses',
'Estimates' => function ($q) {
return $q->limit(5);
},
'Estimates.EstimateStatuses',
],
]);
I am not doing any further transformation of the data. There are no Collection functions being employed here. Nevertheless, in the estimates returned, there is always a mystery, numerically-indexed and empty array element:
[1] => Visualize\Model\Entity\Estimate Object
(
[id] => 30
[user_id] => 1
[estimate_status_id] => 5
[estimate_po] => lksjdf
[estimate_total] =>
[name] => Full throttle
[instore_date] => Cake\I18n\FrozenDate Object
(
[date] => 2022-12-31 00:00:00.000000
[timezone_type] => 3
[timezone] => EST
)
[notes] => Let's do this.
[created] => Cake\I18n\FrozenTime Object
(
[date] => 2022-04-19 13:20:39.000000
[timezone_type] => 3
[timezone] => EST
)
[modified] => Cake\I18n\FrozenTime Object
(
[date] => 2022-04-19 13:21:26.000000
[timezone_type] => 3
[timezone] => EST
)
[1] =>
[location_info] => Array
(
)
[[new]] =>
[[accessible]] => Array
(
[user_id] => 1
[estimate_status_id] => 1
[estimate_po] => 1
[estimate_total] => 1
[name] => 1
[instore_date] => 1
[notes] => 1
[created] => 1
[modified] => 1
[user] => 1
[line_items] => 1
[orders] => 1
[estimate_options] => 1
)
[[dirty]] => Array
(
)
[[original]] => Array
(
)
[[virtual]] => Array
(
[item_count] => 1
[box_count] => 1
[deliverable_count] => 1
[0] => location_info
[estimate_total] => 1
)
[[hasErrors]] =>
[[errors]] => Array
(
)
[[invalid]] => Array
(
)
[[repository]] => Estimates
)
This only affects the Estimates part of the query! The other two elements do not have this mystery index. Here's what CakePHP ultimately uses as it's query, which again, seems fine:
SELECT
Estimates.id AS Estimates__id,
Estimates.user_id AS Estimates__user_id,
Estimates.estimate_status_id AS Estimates__estimate_status_id,
Estimates.estimate_po AS Estimates__estimate_po,
Estimates.estimate_total AS Estimates__estimate_total,
Estimates.name AS Estimates__name,
Estimates.instore_date AS Estimates__instore_date,
Estimates.notes AS Estimates__notes,
Estimates.created AS Estimates__created,
Estimates.modified AS Estimates__modified,
EstimateStatuses.id AS EstimateStatuses__id,
EstimateStatuses.name AS EstimateStatuses__name,
EstimateStatuses.description AS EstimateStatuses__description,
EstimateStatuses.created AS EstimateStatuses__created,
EstimateStatuses.modified AS EstimateStatuses__modified
FROM
estimates Estimates
LEFT JOIN estimate_statuses EstimateStatuses ON EstimateStatuses.id = Estimates.estimate_status_id
WHERE
Estimates.user_id in (1)
ORDER BY
Estimates.modified DESC
LIMIT
5
For obvious reasons, I really need the DebugKit to work, but I have no idea where this mystery element is coming from. Everything seems right, to me?
Edit: Adding the definition of my estimate entity:
<?php
declare(strict_types=1);
namespace Visualize\Model\Entity;
use Cake\ORM\Entity;
/**
* Estimate Entity
*
* #property int $id
* #property int $user_id
* #property int|null $estimate_status_id
* #property string|null $estimate_po
* #property float|null $estimate_total
* #property string $name
* #property \Cake\I18n\FrozenDate $instore_date
* #property string $notes
* #property \Cake\I18n\FrozenTime $created
* #property \Cake\I18n\FrozenTime $modified
*
* #property \Visualize\Model\Entity\User $user
* #property \Visualize\Model\Entity\LineItem[] $line_items
* #property \Visualize\Model\Entity\Order[] $orders
* #property \Visualize\Model\Entity\EstimateOption[] $estimate_options
* #property float $item_count
* #property int $box_count
* #property array|\Cake\Collection\CollectionInterface|null $by_location
* #property int $deliverable_count
* #property array $location_info
* #property \Visualize\Model\Entity\EstimateStatus|null $estimate_status
* #property array|\Cake\Collection\CollectionInterface|null $by_deliverable
*/
class Estimate extends Entity
{
/**
* Fields that can be mass assigned using newEntity() or patchEntity().
*
* Note that when '*' is set to true, this allows all unspecified fields to
* be mass assigned. For security purposes, it is advised to set '*' to false
* (or remove it), and explicitly make individual fields accessible as needed.
*
* #var array
*/
protected $_accessible = [
'user_id' => true,
'estimate_status_id' => true,
'estimate_po' => true,
'estimate_total' => true,
'name' => true,
'instore_date' => true,
'notes' => true,
'created' => true,
'modified' => true,
'user' => true,
'line_items' => true,
'orders' => true,
'estimate_options' => true,
];
/**
* #var array
*/
protected $_virtual = [
'item_count' => true,
'box_count' => true,
'deliverable_count' => true,
'location_info' => true,
'estimate_total' => true,
];
/**
* Returns a zero-filled version of the cannonical ID
*
* #param int $id The autoincremental ID
* #return string
*/
protected function _getId($id): string
{
return sprintf('%05d', $id);
}
/**
* Returns the sum of quantities of all line items.
*
* #return float
*/
protected function _getItemCount(): float
{
$count = 0;
if (!empty($this->line_items)) {
$collection = new \Cake\Collection\Collection($this->line_items);
$count = $collection->sumOf('quantity');
}
return (float)$count;
}
/**
* Returns the count of locations in the estimate
*
* #return int
*/
protected function _getBoxCount(): int
{
$count = 0;
if (!empty($this->line_items)) {
$collection = new \Cake\Collection\Collection($this->line_items);
$collection = $collection->combine('location_id', 'id');
$count = $collection->count();
}
return $count;
}
/**
* Returns the total number of deliverable types in the order.
*
* #return int
*/
protected function _getDeliverableCount(): int
{
$count = 0;
if (!empty($this->line_items)) {
$collection = new \Cake\Collection\Collection($this->line_items);
$collection = $collection->combine('deliverable_id', 'id');
$count = $collection->count();
}
return $count;
}
/**
* Returns the list of locations for this estimate
*
* #return array
*/
protected function _getLocationInfo(): array
{
$info = [];
if (!empty($this->line_items)) {
$collection = new \Cake\Collection\Collection($this->line_items);
$info = $collection->combine(
'location_id',
function ($entity) {
return $entity->location;
}
);
return $info->toArray();
}
return $info;
}
/**
* Returns either the name or the id of the given estimate
*
* #return string
*/
protected function _getName(): string
{
return $this->name ?? (string)$this->id;
}
public const FIELD_ID = 'id';
public const FIELD_USER_ID = 'user_id';
public const FIELD_ESTIMATE_STATUS_ID = 'estimate_status_id';
public const FIELD_ESTIMATE_PO = 'estimate_po';
public const FIELD_ESTIMATE_TOTAL = 'estimate_total';
public const FIELD_NAME = 'name';
public const FIELD_INSTORE_DATE = 'instore_date';
public const FIELD_NOTES = 'notes';
public const FIELD_CREATED = 'created';
public const FIELD_MODIFIED = 'modified';
public const FIELD_USER = 'user';
public const FIELD_LINE_ITEMS = 'line_items';
public const FIELD_ORDERS = 'orders';
public const FIELD_ESTIMATE_OPTIONS = 'estimate_options';
public const FIELD_ITEM_COUNT = 'item_count';
public const FIELD_BOX_COUNT = 'box_count';
public const FIELD_BY_LOCATION = 'by_location';
public const FIELD_DELIVERABLE_COUNT = 'deliverable_count';
public const FIELD_LOCATION_INFO = 'location_info';
public const FIELD_ESTIMATE_STATUS = 'estimate_status';
public const FIELD_BY_DELIVERABLE = 'by_deliverable';
}
Take a close look at the dump of the entity, that field is apparently configured as an exposed virtual field, and looking at the other exposed fields, they seem to be configured incorrectly, as that property should hold a flat array of strings, not string indexed entries like 'item_count' => 1, as the value declares the field name, hence why you see a field named 1.
You are also misinterpreting the output, location_info does not belong to the 1 field, that's actually two different fields, 1 which holds something empty-ish that PHP doesn't print, like null or false, and location_info which holds an empty array.
Use debug() instead of pr()/print_r() to get a better structured dump which also properly shows empty data. And then inspect your Estimate entity class (or wherever things may be configured on the fly) and fix up the virtual fields.

Laravel 8.1 how to make seeder of user table

I am trying to seed the user table but I am facing some issues can someone guide me where I am missing.
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeds.
*
* #return void
*/
public function run()
{
DB::table('users')->insert([
'name' => Str::random(10),
'email' => Str::random(10).'#example.com',
'password' => Hash::make('password'),
]);
}
}
I think you are missing the below line
use Illuminate\Support\Str;
The complete code look like
namespace Database\Seeders;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
class DatabaseSeeder extends Seeder
{
/**
* Run the database seeders.
*
* #return void
*/
public function run()
{
DB::table('users')->insert([
'name' => Str::random(10),
'email' => Str::random(10).'#gmail.com',
'password' => Hash::make('password'),
]);
}
}
Try something like this using eloquent instead of the DB class with an existence check too:
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class UsersTableSeeder extends Seeder
{
/**
* Run the database seeds.
*
* #return void
*/
public function run()
{
// Seed test user 1
$seededAdminEmail = 'admin#admin.com';
$user = User::where('email', '=', $seededAdminEmail)->first();
if ($user === null) {
$user = User::create([
'name' => 'Admin',
'email' => $seededAdminEmail,
'password' => Hash::make('password'),
]);
}
// Seed test user 2
$user = User::where('email', '=', 'user#user.com')->first();
if ($user === null) {
$user = User::create([
'name' => 'User',
'email' => 'user#user.com',
'password' => Hash::make('password'),
]);
}
}
}

Laravel Factories - Create or Make dynamic

I have simple question.
How could I write a Factory that let me defines relationships that use make() or create() depending on original call make() or create()?
It is my use case:
I have a simple factory
/** #var $factory Illuminate\Database\Eloquent\Factory */
$factory->define(App\User::class, function (Faker $faker) {
return [
'role_id' => factory(Role::class),
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'password' => 'secret',
];
});
My problem is with that role_id. When using factory(Role::class), it will create a Role always! Writing in Database...
In my tests, when I write factory(User::class)->create(); It will create an User and Role, thats ok!
But if I write factory(User::class)->make(); It will still create a Role... I wont write in Database a Role when using make(), in that case, I just want a simple role_id => 0.
How could I achieve that?
Thanks!
Edit (Workaround)
Well, it was harder than expected, there is no way to know when using make() or create() when your are defining nested relationships... The only way I found is using debug_stacktrace() method, and look for that make method called from Factory.
For simplification and reusability, I created a helper method for Factories called associateTo($class), this method will define relationships in Factory. It will return an existing ID from related model when using create() or 1 when using make().
By this way, Factories can be:
/** #var $factory Illuminate\Database\Eloquent\Factory */
$factory->define(App\User::class, function (Faker $faker) {
return [
'role_id' => associateTo(Role::class), // Defining relationship
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'password' => 'secret',
'remember_token' => Str::random(10),
];
});
The helper function is:
/**
* Helper that returns an ID of a specified Model (by class name).
* If there are any Model, it will grab one randomly, if not, it will create a new one using it's Factory
* When using make() it wont write in Database, instead, it will return a 1
*
* #param string $modelClass
* #param array $attributes
* #param bool $forceNew
* #return int
*/
function associateTo(string $modelClass, array $attributes = [])
{
$isMaking = collect(debug_backtrace())
// A function called 'make()'
->filter(fn ($item) => isset($item['function']) && $item['function'] === 'make')
// file where I have global functions
->filter(fn ($item) => isset($item['file']) && Str::endsWith($item['file'], 'functions.php'))
->count();
if ($isMaking) return 1;
/** #var Model $model */
$model = resolve($modelClass);
return optional($model::inRandomOrder()->first())->id
?? create($modelClass, $attributes)->id; // call to Factory
}
With this, I can write Unit test easily (using Factories) without the need of using Database connections, so Unit Tests becomes superfast!
I hope this help someone else!
You can overwrite attribute role_id:
factory(User::class)->make(['role_id' => 0]);
Another solution - helper method for any attribute with _id ending:
function make($class)
{
$attributes = \Schema::getColumnListing((new $class)->getTable());
$exclude_attribures = [];
foreach ($attributes as $attribute)
{
if(ends_with($attribute, '_id'))
{
$exclude_attribures[] = [$attribute => 0];
}
}
return factory($class)->make($exclude_attribures);
}
Call example:
make(User:class);
You can take advantage of states
/** #var $factory Illuminate\Database\Eloquent\Factory */
$factory->define(App\User::class, function (Faker $faker) {
return [
'role_id' => factory(Role::class),
'name' => $faker->name,
'email' => $faker->unique()->safeEmail,
'password' => 'secret',
];
});
$factory->state(App\User::class, 'withoutRelationship', [
'role_id' => null,
'another_id' => null,
]);
Then
factory(User::class)->state('withoutRelationship')->make();

Magento 2 adminhtml multiselect and showing selected options after save

How do I get the Magento2 Adminhtml Form with a Multiselect to select the saved options?
Here is my Adminhtml Form Class for reference.
namespace RussellAlbin\Blog\Block\Adminhtml\Post\Edit;
/**
* Adminhtml blog post edit form
*/
class Form extends \Magento\Backend\Block\Widget\Form\Generic
{
/**
* #var \RussellAlbin\Blog\Model\Category\Source\ListCategories
*/
protected $_categories;
/**
* #var \RussellAlbin\Blog\Model\Postcategory
*/
protected $_postcategory;
/**
* #var \Magento\Store\Model\System\Store
*/
protected $_systemStore;
/**
* #param \Magento\Backend\Block\Template\Context $context
* #param \Magento\Framework\Registry $registry
* #param \Magento\Framework\Data\FormFactory $formFactory
* #param \Magento\Store\Model\System\Store $systemStore
* #param \RussellAlbin\Blog\Model\Category\Source\ListCategories $categories
* #param \RussellAlbin\Blog\Model\Postcategory $postcategory
* #param array $data
*/
public function __construct(
\Magento\Backend\Block\Template\Context $context,
\Magento\Framework\Registry $registry,
\Magento\Framework\Data\FormFactory $formFactory,
\Magento\Store\Model\System\Store $systemStore,
\RussellAlbin\Blog\Model\Category\Source\ListCategories $categories,
\RussellAlbin\Blog\Model\Postcategory $postcategory,
array $data = []
) {
$this->_categories = $categories;
$this->_systemStore = $systemStore;
$this->_postcategory = $postcategory;
parent::__construct($context, $registry, $formFactory, $data);
}
/**
* Init form
*
* #return void
*/
protected function _construct()
{
parent::_construct();
$this->setId('blog_post_form');
$this->setTitle(__('Blog Post Information'));
}
/**
* Prepare form
*
* #return $this
*/
protected function _prepareForm()
{
/** #var \RussellAlbin\Blog\Model\Post $model */
$model = $this->_coreRegistry->registry('blog_post');
/** #var \Magento\Framework\Data\Form $form */
$form = $this->_formFactory->create(
['data' => ['id' => 'edit_form', 'action' => $this->getData('action'), 'method' => 'post']]
);
$form->setHtmlIdPrefix('post_');
$fieldset = $form->addFieldset(
'base_fieldset',
['legend' => __('General Information'), 'class' => 'fieldset-wide']
);
if ($model->getPostId()) {
$fieldset->addField('post_id', 'hidden', ['name' => 'post_id']);
}
// Gather our existing categories
$currentCategories = $this->_getExistingCategories( $model );
// Get all the categories that in the database
$allCategories = $this->_categories->toOptionArray();
$field = $fieldset->addField(
'blog_categories',
'multiselect',
[
'label' => __('Categories'),
'required' => true,
'name' => 'blog_categories',
'values' => $allCategories,
'value' => $currentCategories
]
);
$form->setValues($model->getData());
$form->setUseContainer(true);
$this->setForm($form);
return parent::_prepareForm();
}
/**
* #param $model
* #return array
*/
private function _getExistingCategories( $model )
{
// Get our collection
$existingCategories = $this->_postcategory->getCollection()
->addFieldToSelect('category_id')
->addFieldToFilter('post_id', $model->getId());
// Setup our placeholder for the array of categories needed to set back on the value of the multiselect
$itemList = array();
foreach($existingCategories as $_item)
{
$itemList[] = $_item['category_id'];
}
return $itemList;
}
}
I looked back at some work I have done in Magento 1.x and I the only way I got this to work before was using javascript and the setAfterElementHtml to set the options that matched. I remember hating this method of getting it accomplished because it seemed like a work around.
I found the issue.
I did not have the values saved on the model.
// This is what shows it as selected on reload
$model->setData('blog_categories', $categories);
$fieldset->addField(
'blog_categories',
'multiselect',
[
'name' => 'blog_categories[]',
'label' => __('Categories'),
'title' => __('Categories'),
'required' => true,
'values' => $optionArray,
'disabled' => false
]
);

Resources