Adding custom columns to Propel model? - php

At the moment I am using the below query:
$claims = ClaimQuery::create('c')
->leftJoinUser()
->withColumn('CONCAT(User.Firstname, " ", User.Lastname)', 'name')
->withColumn('User.Email', 'email')
->filterByArray($conditions)
->paginate($page = $page, $maxPerPage = $top);
However I then want to add columns manually, so I thought this would simply work:
foreach($claims as &$claim){
$claim->actions = array('edit' => array(
'url' => $this->get('router')->generate('hera_claims_edit'),
'text' => 'Edit'
)
);
}
return array('claims' => $claims, 'count' => count($claims));
However when the data is returned Propel or Symfony2 seems to be stripping the custom data when it gets converted to JSON along with all of the superflous model data.
What is the correct way of manually adding data this way?

To export virtual columns to out array you can do it the next way:
/**
* Propel result set
* #var \PropelObjectCollection
*/
$claims = ClaimQuery::create('c')-> ... ->getResults();
/**
* Array of data with virtual columns
* #var array
*/
$claims_array = array_map(function (Claim $claim) {
return array_merge(
$claim->toArray(), // using "native" export function
array( // adding virtual columns
'Email' => $claim->getVirtualColumn('email'),
'Name' => $claim->getVirtualColumn('name')
)
);
}, $claims->getArrayCopy()); // Getting array of `Claim` objects from `PropelObjectCollection`
unset($claims); // unsetting unnecessary object if we have further operations to complete

The answer to this lies in the toArray() method so:
$claims = ClaimQuery::create('c')
->leftJoinUser()
->withColumn('CONCAT(User.Firstname, " ", User.Lastname)', 'name')
->withColumn('User.Email', 'email')
->filterByArray($conditions)
->paginate($page = $page, $maxPerPage = $top)->getResults()->toArray();
Then you can modify as required, the only issue here is that the current toArray method does not return virtual columns so you would have to patch the method to include them. (This is in the PropelObjectCollection class)
In the end I decided to separate the parts:
return array(
'claims' => $claims,
'count' => $claims->count(),
'actions' => $this->actions()
);
This way you do not have to worry about the virtual columns being lost and jut have to manipulate your data in different ways on the other end.

Related

Creaty an single array by a Laravel Collection, but with specific fields

I have this Model where I'm calling with
$data = ProcessoSeletivoRDSincroniza::all();
This model gaves me a collection with more than 300 records, with attributes like name, celphone, email etc..
And I have to pass this collection to a API body request, by an array, this array has specific key fields, and the only way that I think about doing this its iterating this collection with a foreach loop, and creating/setting this array with this collection fields, and works ok, but my application does one request for every record, and this is not a good way to handle it.
So I'm thinking if's there a way to create an "custom" and single array with all that records, so I dont need to iterate and make a request by record, and just tranform all this records in a JSON file and send it.
This is my code now:
$data = ProcessoSeletivoRDSincroniza::all();
//$data = $data->toArray();
$api = new RDStationApi();
foreach($data as $row)
{
$events = array(
"event_type" => "CONVERSION",
"event_family" => "CDP",
"payload" => [
"conversion_identifier" => "Name of the conversion event",
"name" => $row->nome_completo,
"email" => $row->email,
"personal_phone" => $row->telefone,
"cf_ps_curso" => $row->ps_curso,
"cf_ps_ano_semestre" => $row->ps_anosemestre,
"cf_ps_data_vestibular_agendado" => $row->ps_data_vestibular_agendado,
"cf_ps_nota_enem" => (string) $row->ps_nota_enem,
"cf_forma_ingresso" => $row->ps_forma_ingresso,
"cf_ps_unidade" => $row->ps_unidade,
"cf_ps_situacao" => $row->ps_situacao
]
);
$return = $api->update_contact($events);
You can use a Laravel functionality called API-Resources.
https://laravel.com/docs/8.x/eloquent-resources
Create a new Resource for your Model:
php artisan make:resource ProcessoSeletivoRDSincronizaResource
Afterwards this will create a file in the Resource folder named; ProcessoSeletivoRDSincronizaResource , in this file you will need to adapt the toArray() method.
<?php
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
class ProcessoSeletivoRDSincronizaResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* #param \Illuminate\Http\Request $request
* #return array
*/
public function toArray($request)
{
return [
'id' => $this->id,
//ADD ALL THE FIELDS, methods also work normally: $this->myMethod()
'created_at' => $this->created_at,
'updated_at' => $this->updated_at,
];
}
}
Afterwards you can use the Resource like this:
//for the whole collection
$events = ProcessoSeletivoRDSincronizaResource::collection(ProcessoSeletivoRDSincroniza::all());
//or for single use
$event = new ProcessoSeletivoRDSincronizaResource($single_model)

How to properly test subclasses with phpspec dataprovider

I'm pretty new to Phpspec testing and I don't know what is the correct way to test multiple scenarios when transforming a object to different response structure.
I need to check if price is correctly calculated. Here I have the Transformer spec test:
/**
* #dataProvider pricesProvider
*/
public function it_should_check_whether_the_prices_are_correct(
$priceWithoutVat,
$priceWithVat,
$vat,
Request $request,
Repository $repository
) {
$productIds = array(100001);
$result = array(
new Product(
'100001',
'MONSTER',
new Price(
$priceWithoutVat,
20,
'GBP',
null,
null
)
)
);
$expected = array(
array(
"productId" => "100001",
"brand" => "MONSTER",
"price" => array(
"amount" => $priceWithVat,
"vatAmount" => $vat,
"currencyCode" => "GBP",
"discountAmount" => (int)0
)
)
);
$repository->getResult(array(
Repository::FILTER_IDS => $productIds
))->willReturn($result);
$request->get('productIds')->willReturn(productIds);
/** #var SubjectSpec $transformedData */
$transformedData = $this->transform($request);
$transformedData->shouldEqual($expected);
}
public function pricesProvider()
{
return array(
array('123.456789', 14814, 2469),
array('60.00', 7200, 1200),
);
}
In my Transformer class I have a function which formats data to the correct format:
public function transform(Request $request)
{
$productIds = $request->get('productIds');
$productsResult = $this->repository->getResult(array(
Repository::FILTER_IDS => $productIds
));
$products = array();
foreach ($productsResult as $product) {
$products[] = $this->formatData($product);
}
return $products;
}
/**
* #param Product $product
* #return array
*/
private function formatData(Product $product)
{
return array(
'productId' => $product->getId(),
'brand' => $product->getBrandName(),
'price' => array(
'amount' => (int)bcmul($product->getPrice()->getAmountWithTax(), '100'),
'vatAmount' => (int)bcmul($product->getPrice()->getTaxAmount(), '100'),
'currencyCode' => $product->getPrice()->getCurrencyCode(),
'discountAmount' => (int)bcmul($product->getPrice()->getDiscountAmount(), '100')
)
);
}
The problem is, that I'm getting this error message:
316 - it should check whether the prices are correct
warning: bcmul() expects parameter 1 to be string, object given in
/src/AppBundle/Database/Entity/Product/Price/Price.php line 49
If I hard-code those values then the test is green. However I want to test varios prices and results, so I decided to use the dataProvider method.
But when dataProvider passes the $amountWithoutTax value, it's not string but PhpSpec\Wrapper\Collaborator class and because of this the bcmul fails.
If I change the $amountWithoutTax value to $priceWithoutVat->getWrappedObject() then Double\stdClass\P97 class is passed and because of this the bcmul fails.
How do I make this work? Is it some banality or did I completely misunderstood the concept of this?
I use https://github.com/coduo/phpspec-data-provider-extension and in composer.json have the following:
"require-dev": {
"phpspec/phpspec": "2.5.8",
"coduo/phpspec-data-provider-extension": "^1.0"
}
If getAmountWithTax() in your formatData method returns an instance of PhpSpec\Wrapper\Collaborator, it means that it returns a Prophecy mock builder instead of the actual mock, i.e. the one that you get by calling reveal() method. I don't know how your data provider looks like, but it seems that you're mocking your Price value objects instead of creating real instances thereof, and $product->getPrice() in your production code returns the wrong kind of object.
The solution would be either to create a real instance of the Price value object that's later returned by $product->getPrice() with new in the data provider, or by calling reveal() on that instance, like this (assuming $price is a mock object that comes from a type hinted parameter):
$product->getPrice()->willReturn($price->reveal());

PHP ElasticSearch how to set mapping before indexing records?

I'm using laravel and elasticsearch-php to index and store data to elastic, my problem is that elastisearch uses from dynamic mapping but I need to set my custom mapping. How can I use from my mapping?
Bellow is my code:
$client = \Elasticsearch\ClientBuilder::create()->build();
$mappingData = array(
'index' => 'promote_kmp',
'body' => array(
'mappings' => $resource->getMappingProperties()
)
);
$client->indices()->create($mappingData);
$params = [
'type' => 'resources',
'id' => uniqid(),
'body' => [
'id' => $resource->id,
'name' => $resource->name,
'display_name_en' => $resource->display_name_en,
'display_name_pr' => $resource->display_name_pr,
'display_name_pa' => $resource->display_name_pa,
'table_name' => $resource->table_name,
'model_name' => $resource->model_name,
'in_sidemenu' => $resource->in_sidemenu,
'icon_class' => $resource->icon_class,
'created_at' => $resource->created_at,
'created_by' => $user,
]
];
//$response = $client->indices()->create($resource->getMappingProperties());
$client->index($params);
$resource->getMappingProperties() get the mapping array I have set in model.
but when I want to index a record it says IndexAlreadyExistsException[[promote_kmp] already exists]. This question arise when I want to search for date field searching is not properly working and I guess that mapping is not true.
As I was saying in comments.
The code is executing the creation of index every time you want to query.
But the index must be created only once.
So it should work like the migration for the DB's.
The only idea I can give you is to make a command to generate the index.
So that you could just
$ artisan elasticsearch:generate <index>
About the code, what I've done for our case, made the index with a way to inject the types, plus a way to create them into elasticsearch:
interface Index {
/**
* #param Type[] $types Index types (resources)
*/
function setTypes(array $types);
/**
* Generate the index and the types into the elasticsearch
*/
function create();
}
Then the types should generate the mappings and the type name (as /<index>/<type>, like:
interface Type {
/**
* #return string The type name
*/
function getName();
/**
* #return array The type mapping
*/
function getMapping();
}
So (somewhere), you would create the class (this could be better):
$myIndex = new MyIndex();
$myIndex->setTypes([
new MyFirstType(),
new MySecondType(),
//...
]);
$myIndex->create();
I hope this helps.

CSV Import in SilverStripe Duplicates and RelationCallbacks

I need to understand the code below, specially how exactly $duplicateChecks and $relationCallbacks work but there is little explanation on the official documentation. Can somebody explain how these work or suggest some other documentation I can look at?
class PlayerCsvBulkLoader extends CsvBulkLoader {
public $columnMap = array(
'Number' => 'PlayerNumber',
'Name' => '->importFirstAndLastName',
'Birthday' => 'Birthday',
'Team' => 'Team.Title',
);
public $duplicateChecks = array(
'Number' => 'PlayerNumber'
);
public $relationCallbacks = array(
'Team.Title' => array(
'relationname' => 'Team',
'callback' => 'getTeamByTitle'
)
);
public static function importFirstAndLastName(&$obj, $val, $record) {
$parts = explode(' ', $val);
if(count($parts) != 2) return false;
$obj->FirstName = $parts[0];
$obj->LastName = $parts[1];
}
public static function getTeamByTitle(&$obj, $val, $record) {
return FootballTeam::get()->filter('Title', $val)->First();
}
}
$duplicateChecks is used by findExistingObject function in the CsvBulkLoader class. It is iterated over to find any object that has a column with the specified value. In that example, it checks the "PlayerNumber" column.
It can also be passed a callback like so:
public $duplicateCheck = array(
'Number' => array(
'callback' => 'checkPlayerNumberFunction'
)
);
The callback specified needs to either exist on an instance of the class specified on the property objectClass or on the CsvBulkLoader itself (which would happen if you extended it). These callbacks are used to do more complex duplicate lookups and return an existing object (if any) found.
$relationCallbacks on the other hand is used by the main processRecord function. The callback works in the same way as the $duplicateCheck callback, it needs to either exist on an instance of the class specified on the proeprty objectClass or on the CsvBulkLoader. These callbacks can return an object that will be related back to a specific object record (new or existing) as a has_one.
There is a little more to it than that though the best way to learn is by a bit of experimentation and jumping through the code of the class itself. I have linked to the various functions etc in my answer.

cakePHP build a select box from custom array

So we have a variety of search pages each with different search criteria, so I decided to write a component which will get parameters passed to it from the controller, collect the neccesary data and return an array which I could then set to be used within my view file to populate drop down boxes to filter the criteria.
I have managed to get everything write up to where I must use the cakePHP helper to build a dynamical select box. I am convinced that I am doing something wrong and if there is an easier way to do this and still keep it somewhat dynamic please assist where you can:
// COMPONENT METHOD:
public function filterQueries($parameters) {
// Get defaults from the user:
$criteria = $parameters["custom"];
$defaults = $parameters["defaults"];
// Validate the defaults the user may want and assign them to the return array:
if($defaults != "false") {
foreach($defaults as $key => $value) {
if(array_key_exists($value, $this->defaults)) {
$this->returnArray["defaults"][$value] = $this->defaults[$value];
}
}
}
// Get all data for the custom requested form fields:
if($criteria != false) {
foreach($criteria as $model => $arguments) {
$fields = $arguments["fields"];
$conditions = $arguments["conditions"];
$recursive = $arguments["recursive"];
if(!in_array($model,$this->uses)) {
$useModel = ClassRegistry::init($model);
} else {
$useModel = $this->$$model;
}
$array = $useModel->find("all",array("conditions" => $conditions, "fields" => $fields, "recursive" => $recursive));
$this->returnArray["custom"][$model] = $array;
}
}
return $this->returnArray;
}
The above function will get an array which breaks down either custom searches or defaults (not included above but it all works, it returns the array exactly as I would have expected it.
// The code within my action to get the content from above:
// Load the Filters component to search data:
$search = $this->Components->load("Filter");
// Tell search what you want:
$searchBoxes = array(
"defaults" => array("statuses", "survey_type"),
"custom" => array(
"User" => array(
"fields" => array("User.id","User.first_name", "User.last_name"),
"conditions" => array("User.user_group_id" => "4f847c63-1840-446e-88be-3e4d29566cf0"),
"recursive" => -1
)
)
);
$filterResults = $search->filterQueries($searchBoxes);
$this->set("filters",$filterResults);
So now I get this multi-associative array within my view file (all still fine), but I want to now build example a drop down list of the users based on the array created above, but the outcome is nothing like what I expected:
echo $this->Form->input('user_id',
array(
"type" => "select",
"options" => $filters["custom"]["User"]
)
);
The HTML output is broken and displays it like this:
<option value="last_name">Doe</option>
<option value="first_name">Jihn</option>
<optgroup label="User"> </optgroup>
<optgroup label="1"> </optgroup>
<option value="last_name">Marilyn</option>
<option value="first_name">Monroe</option>
I acknowledge that I do not have a lot of cake experience but cannot understand how to just get the results to :
<option value='USERID'>NAME</option> // Yes I know the names and surnames must be concatinated still
Any advise help or guidance on how to do it, and do it the right way, would greatly be appreciated :)
VARDUMP ON $filters['custom']['users']
array
0 =>
array
'User' =>
array
'id' => string '4f84840e-cda8-4704-8fdf-210729566cf0' (length=36)
'first_name' => string 'Name' (length=4)
'last_name' => string 'Surname' (length=11)
1 =>
array
'User' =>
array
'id' => string '4f8488cb-53e0-4f72-af73-3de229566cf0' (length=36)
'first_name' => string 'Name' (length=6)
'last_name' => string 'Surname' (length=6)
You can enhance your output by doing as follows:
1) for combining two fields of a table, you can use "virtualfields" in the model, as follows: For example if you have the user model, you can define as follows:
public $virtualFields = array(
'full_name' => 'CONCAT(first_name, " ",last_name)'
);
So now the "full_name" field will be got whenever you call the find method of the User model.
2) For getting the data from the table for a select box, you can use the find('list') method. For example for the User model if you need the id,full_name (last and first name combined using the virtual fields) of the table,it can be done as follows :
$this->User->find('list',array('fields'=>array('id','full_name'),'conditions'=>$conditions))
I hope this helps..
Well I guess what you want to do is actually create another array with formatted options.
foreach ($filters["custom"]["User"] as $arr)
{
$options[$arr['id']] = $arr['first_name'] . ' ' . $arr['last_name'];
}
then
echo $this->Form->select('user_id', $options);
I think you need something like this:
$result = Set::combine($filters["custom"]["User"],
'{n}.User.id', // this is the value of select box
array(
'{0} {1}',
'{n}.User.first_name',
'{n}.User.last_name'
) // this is the text of select box
);
pr($result);
$this->form->input('inputname', array('label' => false, 'options' => array('Select optionstype','a','b'), 'class'=>array('form-a', 'b'), 'id' => 'bacid', 'style' => 'width:280px;','value'=>$abc['model']['array_attribute']));
On cakePHP 3, I can't use "virtualFields" yet, but I can use as follows:
//In PostsController
$users = $this->Posts->Users->find('list',
['keyField' => 'id',
'valueField' => 'full_name', //Don't forget to call concatened field here
'conditions' => $conditions,
'order' => $orderBy
]);
//here the magic happens
$concat = $users->func()->concat(
['Users.first_name' => 'identifier',
' ',
'Users.last_name' => 'identifier'
]);
//selecting fields
$users->select(['id', 'full_name' => $concat]);
//sending to view
$this->set('users', $users->toArray());
I hope this helps CakePHP 3 developers

Categories