Laravel Unit Testing with MongoDB and Object arrays - php

Hello I'm pretty new to PHPUnit and Laravel 4.
I've created an REST API and I'm trying to add a new User using laravel (PHPUnit) unit tests, to my test DB. The problem is, that it completely disregards the Object array that I'm trying to add with the user doc creation.
A typical User Doc would look as follows:
{
_id": ObjectId("53021089b4d15220118b4568"),
"oArr": {
"something": {
"somewhere": "5th Avenue",
"ip": "192.168.0.20"
},
"something2": {
"somewhere": "6th Avenue",
"ip": "192.168.0.21"
}
}
"email": "jessica#example.com",
"name": "Jessica Alba",
"password": "$2y$10$RAVzUGLAG.81IOOUum0k0u5vrcY98H.L42FeSJekEywUCV.ycttn6"
}
Now Laravel has it's own Testing class TestCase which your Unit tests extend.
Typical example would be the following:
class AddUserTest extends TestCase {
/**
* A basic functional test example.
*
* #return void
*/
public function testCreateUsers()
{
$post_data = array(
'email' => 'emma#example.com',
'name' => 'Emma Watson',
'password' => 'password',
'oArr' => array(
'something' => array(
'somewhere' => '7th Avenue',
'ip' : => '192.168.0.31'
),
),
);
}
}
Once the user test has run (successfully), the new Emma Watson user (Mongo Doc) look as follows:
{
_id": ObjectId("53021089b4d15220118b4568"),
"email": "emma#example.com",
"name": "Emma Watson",
"password": "$2y$10$asdAzUGLAG.8asvWQOUum0k0u5vrcY98H.LEDeSJekEywUCV.ycttn6"
}
I have no idea how, and couldn't find anyone else with the same problem.
My assumption: The ORM I'm using (https://github.com/jenssegers/Laravel-MongoDB), doesn't cater for Object arrays in Unit Tests.
I have yet to look into how the ORM is handling unit tests, I'm just hoping someone else has had the same problem, and has a more basic solution.

Well, your test returned modified document. Mongodb is automatically appending new _id to every insertion, that is not explicitly marked as update. Also mongo is hashing you password - it is probably done by you package driver. Since mongodb is using nested document, you test probably didn't reach that nested level, and there is no that document.

Completely forgot about this post.
Laravel's Input class handles for JSON input also. So if you're trying to post embedded documents, you might as well do it in an JSON format. The following worked for me.
class AddUserTest extends TestCase {
/**
* A basic functional test example.
*
* #return void
*/
public function testCreateUsers()
{
$post_data = array(
'email' => 'emma#example.com',
'name' => 'Emma Watson',
'password' => 'password',
'oArr' => array(
'something' => array(
'somewhere' => '7th Avenue',
'ip' : => '192.168.0.31'
),
),
);
$post_data = json_encode($post_data);
// Send through the serialized data to the Controller.
}
}

Related

In Laravel can I set a default context for the Log facade

I'm using the Log:: facade a lot and have a helper class called LogHelper which provide me with a static method LogHelper::context() which include many key values I need to track the requests. But having to type it every time for each usage make it error prune and fill not so efficient.
I'm looking for a way to inject the values by default, and allow me to overwrite them if needed specifically.
At the moment this is how I use it,
Log::debug('Request Started', LogHelper::context());
what I'm looking for is to inject the context by default
Log::debug('Request Started');
and have the option to overwrite it, if need it:
Log::debug('Request Started', ['more' => 'context'] + LogHelper::context());
PS, the LogHelper::context() return a simple key => value array which include some staff i need to debug requests, and the reason it do not use the values directly in the message is because i log to graylog as structured data, and this way i can filter by any key.
I have solved this issue by using the tap functionality and $logger->withContext() (note: the latter was added in Laravel 8.49).
You want to create a new class which contains your context logic. I've created an extra Logging folder in app/ in which my logging customizations sit.
app/Logging/WithAuthContext.php:
<?php
namespace App\Logging;
use Illuminate\Log\Logger;
class WithAuthContext
{
public function __invoke(Logger $logger)
{
$logger->withContext([
'ip' => request()?->ip(),
'ua' => request()?->userAgent(),
]);
}
}
Depending on which logging channel(s) you use, you will have to add the class to each one you want to add context to. So in app/config/logging.php:
<?php
use App\Logging\WithAuthContext;
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
return [
// ...
'channels' => [
// ...
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'tap' => [WithAuthContext::class],
],
// ...
],
];
There is a way, but it is not pretty. You can create a custom monolog logger driver. The process is described at https://laravel.com/docs/8.x/logging#creating-monolog-handler-channels.
Here's a possible implementation:
class ContextEnrichingLogger extends \Monolog\Handler\AbstractHandler {
private $logger;
public function __construct($level = Monolog\Logger::DEBUG, bool $bubble = true, $underlyingLogger = 'single') {
$this->logger = Log::driver($underlyingLogger);
}
public function handle(array $record) {
$record['context'] += LogHelper::context();
return $this->logger->handle($record);
}
}
Then register this as a custom logger in your config/logging.php:
return [
'default' => 'enriched',
//...
'channels' => [
// ...
'enriched' => [
'driver' => 'monolog',
'handler' => ContextEnrichingLogger::class,
'level' => env('APP_LOG_LEVEL', 'debug'),
"with" => [
"underlyingLogger" => env('LOG_CHANNEL', 'single')
]
]
]
];
I haven't tested this particular one but this is how I've defined other custom loggers.
Note, this is probably also achievable via a custom formatter though I think it's probably the same trouble.

Laravel Model Factory - Large JSON Stubs

I am pulling order information from an external API and saving the entire JSON response into a documents table.
I obviously do not want to call this API when testing so I have setup a factory, but I am struggling with how to pull in these JSON stubs. For example:
// factory
$factory->define(App\OrderDocument::class, function (Faker $faker) {
return [
'document_id' => $faker->uuid,
'status' => $faker->randomElement(['open', 'partial', 'processed', 'updating']),
'document' => $faker->text
];
});
// Currently using it like...
foreach ($this->fullDocuments() as $id => $document){
$a = factory(\App\OrderDocument::class)->create([
'document_id' => $id,
'status' => 'full'
]);
$a->setRawAttributes(['document' => $document]);
$a->save();
}
where $this->fullDocuments() contains an array of the document_id and raw JSON response from the API.
I know a better way to do this is to factory the entire JSON document. The JSON contains about 500 lines so it would be very time consuming but also I do not own this data, so I assume I should not be trying to fake it.
Within my test, I would prefer to do something like the below, but am not sure how.
factory(OrderDocument::class, 10)->create([
'document_id' => $this->getDocumentId($i++),
'document' => $this->getDocumentStub($i++),
]);

Apigility + Doctrine - ManyToMany fetch Eager not working from either side - data present

I have a setup with Country and Currency objects. A Country may have 0 or more Currency objects and vice versa there's the Currency that may be used by 0 or more Country objects. The problem is the data is not returned to the front-end (api requests).
In Country Entity
/**
* #var Collection|ArrayCollection|Currency[]
* #ORM\ManyToMany(targetEntity="Currency", inversedBy="countries", fetch="EAGER")
* #ORM\JoinTable(
* name="country_country_currencies",
* joinColumns={#ORM\JoinColumn(name="country_id", referencedColumnName="id")},
* inverseJoinColumns={#ORM\JoinColumn(name="currency_id", referencedColumnName="id")}
* )
*/
protected $currencies;
In Currency Entity
/**
* #var Collection|ArrayCollection|Currency[]
* #ORM\ManyToMany(targetEntity="Country", mappedBy="currencies", fetch="EAGER")
*/
protected $countries;
Both sides have a __construct() function setting the initial values to new ArrayCollection(). Both have their own get*(), add*() and remove*() functions.
When debugging the DoctrineResource::fetch() function on a single object (e.g. /currencies/45) the $entity object does contain the data when executing return $entity (last line of fethc()), so I know the data is there.
However, when it finally returns to the "front-end", I'm missing the data of the ManyToMany relation:
As you can see above: countries is empty.
When requesting from the other side (from Country), the currencies is empty.
Additional info
Other similar questions had suggestions in comments or answers I'll put here straight away as I already checked those.
Doctrine caching - I ran the following commands:
./vendor/bin/doctrine-module orm:clear-cache:query
./vendor/bin/doctrine-module orm:clear-cache:result
./vendor/bin/doctrine-module orm:clear-cache:metadata
I've made sure that Zend Framework caching is disabled (also, development mode is enabled via composer development-enable).
The Entity proxies generated have also been deleted multiple times to make sure they aren't the issue. I've tried also without fetch="EAGER" (without is the original actually), but that yields the same result.
Using wrong hydrator for Entities: both are configured to use the Doctrine EntityManager (same for both):
'zf-hal' => [
'metadata_map' => [
\Country::class => [
'route_identifier_name' => 'id',
'entity_identifier_name' => 'id',
'route_name' => 'country.rest.doctrine.country',
'hydrator' => 'Country\\V1\\Rest\\Country\\CountryHydrator',
'max_depth' => 3,
],
\Country\V1\Rest\Country\CountryCollection::class => [
'entity_identifier_name' => 'id',
'route_name' => 'country.rest.doctrine.country',
'is_collection' => true,
],
// other entity
],
],
// some more config
'zf-apigility' => [
'doctrine-connected' => [
\Country\V1\Rest\Country\CountryResource::class => [
'object_manager' => 'doctrine.entitymanager.orm_default',
'hydrator' => 'Country\\V1\\Rest\\Country\\CountryHydrator',
],
// other entity
],
],
'doctrine-hydrator' => [
'Country\\V1\\Rest\\Country\\CountryHydrator' => [
'entity_class' => \Salesupply\Core\Country\Entity\Country::class,
'object_manager' => 'doctrine.entitymanager.orm_default',
'by_value' => true,
'strategies' => [],
'use_generated_hydrator' => true,
],
// other entity
],
Another suggested the content negotiation type might be set to json instead of HalJson: both (everything really) is set to HalJson:
'zf-content-negotiation' => [
'controllers' => [
'Country\\V1\\Rest\\Country\\Controller' => 'HalJson',
'Country\\V1\\Rest\\Currency\\Controller' => 'HalJson',
],
],
The ApiSkeletons vendor package has this by design. I've opened an issue on Github some months back.
To ensure you receive back collections:
Create a strategy class and extend the AllowRemoveByValue strategy of Doctrine.
Overwrite the extract function to return a Collection, either filled or empty
That's it.
Full class:
namespace Application\Strategy;
use DoctrineModule\Stdlib\Hydrator\Strategy\AllowRemoveByValue;
use ZF\Hal\Collection;
class UniDirectionalToManyStrategy extends AllowRemoveByValue
{
public function extract($value)
{
return new Collection($value ?: []);
}
}
Apply this strategy where you need it. E.g. your Advert as many Applications, so the config should be modified like so:
'doctrine-hydrator' => [
'Application\\V1\\Entity\\ApplicationHydrator' => [
// other config
'strategies' => [
'relatedToManyPropertyName' => \Application\Strategy\UniDirectionalToManyStrategy::class,
],
],
],
Now collections should be returned.
Quick note: this will only work as a strategy for the *ToMany side.

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.

php Restler Routes

I am using Restler 2.0 and I'm trying to add a new route based on the CRUD example
$o['GET']['author/:name/:email']=array (
'class_name' => 'Author',
'method_name' => 'getLogin',
'arguments' =>
array (
'name' => 0,
'email' => 1,
),
'defaults' =>
array (
0 => NULL,
1 => NULL,
),
'metadata' =>
array (
),
'method_flag' => 0,
);
when I make the url call in the browser http://[host]/author/[name to pull]/[email to pull]
I get the following error:
{
"error": {
"code": 404,
"message": "Not Found"
}
}
my author code has been updated with the following method
function getLogin($name=NULL,$email=NULL) {
print "in author, getting login";
return $this->dp->getLogin($name,$email);
}
I am stumped.
Luracast Restler Auto Routing
Firstly, routes.php is auto generated when you run Restler in production mode
$r = new Restler(TRUE);
which will be overwritten when we call
$r->refreshCache();
or run it in debug mode, so it should not be hand coded.
Restler 2.0 is using auto mapping which is better explained in the updated CRUD Example.
Corrected version of your method should be
function get($name=NULL,$email=NULL) {
print "in author, getting login";
return $this->dp->getLogin($name,$email);
}
which will map to
GET /author/:email/:password
where as your method is currently mapping to
GET /author/login/:email/:password
Luracast Restler Custom Routing
Also note that you can use PHPDoc comment to create custom mappings and you can add more than one. For example
/*
* #url GET /custom/mapping/:name/:email
* #url GET /another/:name/:email
*/
function get($name=NULL,$email=NULL) {
print "in author, getting login";
return $this->dp->getLogin($name,$email);
}
this will create the following routes, and disable auto routing for that method.
GET /author/custom/mapping/:email/:password
GET /author/another/:email/:password

Categories