How to properly test subclasses with phpspec dataprovider - php

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());

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)

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.

Adding Cart functions for Amazon API

I am trying to add cart functions to the AmazonECS class available at https://github.com/Exeu/Amazon-ECS-PHP-Library
The main class of that project is https://github.com/Exeu/Amazon-ECS-PHP-Library/blob/master/lib/AmazonECS.class.php
It currently supports ItemLookup and ItemSearch but it does not have CartCreate, CartClear, CartAdd, CartGet, CartModify.
Amazon's documentation about these API calls can be found on this page
http://docs.aws.amazon.com/AWSECommerceService/2011-08-01/DG/CartCreate.html
Here's one of the things I have tried which hasn't worked.
/**
* execute CartCreate request
*
* #param string $asin, $associateTag
*
* #return array|object return type depends on setting
*
* #see returnType()
*/
public function cartCreate($asin, $associateTag)
{
$params = $this->buildRequestParams('CartCreate', array(
array('Item.1.ASIN' => $asin, 'Item.1.Quantity' => 1),
'AssociateTag' => $associateTag,
));
return $this->returnData($this->performSoapRequest("CartCreate", $params));
}
Does anyone know what I'm doing wrong? The error message I get back from that call is
string(79) "Your request is missing required parameters. Required parameters include Items."
For anyone who is still searching for an answer to this, I have this solution for you. The following is my cart function:
public function CartCreate($OfferListingId)
{
$params = $this->buildRequestParams('CartCreate', array(
'Items' => array(
'Item' => array(
//You can also use 'ASIN' here
'OfferListingId' => $OfferListingId,
'Quantity' => 1,
)
)
));
return $this->returnData(
$this->performSoapRequest("CartCreate", $params)
);
}
The parameters you send should be constructed as an associated array.
Also, in case you get an error about not being able to add the item with ASIN to the cart, remember to add the country code to the Request, since the default is US and, for example, in my case I needed items from UK. This is how I do my Request:
$cart = $this->ecs->country('UK')->ResponseGroup('Cart')->CartCreate($OfferListingId);
I may be wrong, but I don't think your Item.1.ID should be part of another array. Try this instead:
$params = $this->buildRequestParams('CartCreate', array(
'Item.1.ASIN' => $asin,
'Item.1.Quantity' => 1,
'AssociateTag' => $associateTag,
));

Incomplete PHP class when serializing object in sessions

I'm working on a shopping cart (Cart model). One of its protected properties is "_items", which holds an array of Product objects. They (Products) all get stored in DB for populating the session (using ZF, Zend_Session_SaveHandler_DbTable() etc.).
public function addItem(Model_Product $product, $qty)
{
$qty = (int) $qty;
$pId = $product->getId();
if ($qty > 0) {
$this->_items[$pId] = array('product' => $product, 'qty' => $qty);
} else {
// if the quantity is zero (or less), remove item from stack
unset($this->_items[$pId]);
}
// add new info to session
$this->persist();
}
In the controller, I grab a Product obj from DB with the ProductMapper and provide it to "addItem()":
$product1 = $prodMapper->getProductByName('cap');
$this->_cart->addItem($product1, 2);
getProductByName() returns a new populated Model_Product object.
I usually get the
Please ensure that the class definition "Model_Product" of the object you are trying to operate on was loaded _before_ ...
error message, a session dump obviously shows
['__PHP_Incomplete_Class_Name'] => 'Model_Product'
I know about the "declaring the class before serializing it". My problem is this: how can I declare the Product class in addItem(), if it's injected (first param) in the first place? Wouldn't a new declaration (like new Model_Product()) overwrite the param (original object) in addItem()? Must I declare it in the Cart model again?
Besides, I'll surely get a Cannot redeclare class Model_Product if I... redeclare it in Cart.
In ZF's bootstrap, the session was started before autoloading.
/**
* Make XXX_* classes available
*/
protected function _initAutoloaders()
{
$loader = new Zend_Application_Module_Autoloader(array(
'namespace' => 'XXX',
'basePath' => APPLICATION_PATH
));
}
public function _initSession()
{
$config = $this->_config->custom->session;
/**
* For other settings, see the link below:
* http://framework.zend.com/manual/en/zend.session.global_session_management.html
*/
$sessionOptions = array(
'name' => $config->name,
'gc_maxlifetime' => $config->ttl,
'use_only_cookies' => $config->onlyCookies,
// 'strict' => true,
// 'path' => '/',
);
// store session info in DB
$sessDbConfig = array(
'name' => 'xxx_session',
'primary' => 'id',
'modifiedColumn' => 'modified',
'dataColumn' => 'data',
'lifetimeColumn' => 'lifetime'
);
Zend_Session::setOptions($sessionOptions);
Zend_Session::setSaveHandler(new Zend_Session_SaveHandler_DbTable($sessDbConfig));
Zend_Session::start();
}
When I was getting the errors I was talking about, the method declaration was the other way around: _initSession() was first, then _initAutoloaders() - and this was the exact order ZF was processing them.
I'll test some more, but this seems to work (and logical). Thanks for all your suggestions.

Categories