Inconsistent Data being associate on belongsTo - php

I am creating a feature test on one to many relationship. I just have a simple one to many relation with a proper setup base on the Laravel documentation. My test goes like this.
/** #test */
public function it_attach_containers()
{
$this->withoutExceptionHandling();
$vendor = factory(Vendor::class)->create();
$containersCount = 30;
$containers = factory(Container::class, $containersCount)->create();
$user = factory(User::class)->create();
$attributes = [
'vendor' => $vendor->id,
'ordered' => null,
'deployed' => null,
'last_contact' => null,
'containers' => $containers->pluck('name')
];
$response = $this->actingAs($user, 'api')
->withHeaders([
'X-Requested-With' => 'XMLHttpRequest'
])
->json('POST', '/api/deployments', $attributes);
$deployment = Deployment::find($containers[0]->id);
$this->assertInstanceOf(Deployment::class, $deployment);
$this->assertCount($containersCount, $deployment->containers()->get());
$this->assertDatabaseHas('deployments', [
'vendor' => $vendor->id,
'ordered' => null,
'deployed' => null,
'last_contact' => null
]);
}
The relation I have is a one to many relationship. A one deployment has many container. The code below is how I associate relation..
public function associateDeployment(Deployment $deployment, $data)
{
foreach ($data['containers'] as $containerName) {
$container = Container::where('name', $containerName)->first();
if (!$container) {
$container = Container::create([
'name' => $containerName,
'status' => true
]);
}
if (is_null($container->deployment_id)) {
$container->deployment()->associate($deployment);
$container->save();
}
}
}
The result on my test is really weird. sometimes it pass but sometimes not. I notice that the issue occur on the assertCount. as you can see on my test. it assert if the containers is 30. but mostly it didnt go up to 30. its about 25-29.. then sometimes it pass. what do you think is the problem?

I think the bug is the following line:
$deployment = Deployment::find($containers[0]->id);
Here you are fetching a deployment record by using container id. Instead use the following code:
$deployment = Deployment::find($containers[0]->deployment_id);

Related

Laravel hasMany is Return No Results

In Laravel, I am creating a message thread feature. My schema looks like this:
MessageThreads Table
column
id
MessageThreadParticapants Table
column
thread_id
user_id
And I have the corresponding models of MessageThread and MessageThreadParticapant. In the MessageThread model, I have the following relation:
public function users() {
return $this->hasMany(MessageThreadParticapant::class, 'thread_id', 'id');
}
Here is where things get funny. If I do:
MessageThread->users
I get an empty result. But if I do:
MessageThreadParticapant::where('thread_id', $same_thread_id)->get()
I get the correct amount of results back. What am I doing wrong here?
UPDATE
One of the suggestions was "hasMany(Model, 'foreign_key', 'local_key')" to be incorrect. Some more context,its failing my unit tests. I'm testing up a test as such:
public function testUsers() {
$thread1 = MessageThread::factory()->create();
$thread2 = MessageThread::factory()->create();
$this->assertCount(0, $thread1->users);
$this->assertCount(0, $thread2->users);
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$user3 = User::factory()->create();
$user4 = User::factory()->create();
MessageThreadParticapant::factory()->create([
'user_id' => $user1->id,
'thread_id' => $thread1->id
]);
MessageThreadParticapant::factory()->create([
'user_id' => $user2->id,
'thread_id' => $thread1->id
]);
MessageThreadParticapant::factory()->create([
'user_id' => $user2->id,
'thread_id' => $thread2->id
]);
MessageThreadParticapant::factory()->create([
'user_id' => $user3->id,
'thread_id' => $thread2->id
]);
MessageThreadParticapant::factory()->create([
'user_id' => $user4->id,
'thread_id' => $thread2->id
]);
//PASSES!!!!
$this->assertCount(2, MessageThreadParticapant::where('thread_id', $thread1->id)->get());
//FAILS!!!
$this->assertCount(2, $thread1->users);
$this->assertCount(3, $thread2->users);
}
At bottom of my test:
//PASSES!!!!
$this->assertCount(2, MessageThreadParticapant::where('thread_id', $thread1->id)->get());
//FAILS!!!
$this->assertCount(2, $thread1->users);
In other tests, $thread->users works correctly in getting the right amount of users back. Why are these getting different results?
I solved this problem in two steps.
Refresh
Lazy loading apparently only represents that state of the object at the time that it was loaded. Meaning it's not retrieving new data from the DB when the joined property is called. To solve, just do a refresh on the model and then access the joined property.
$model->refresh();
$model->users;
String ID
I'm using UUID in Postegresql. Even those I am using $cast = ['id' => 'string']; in model, this is not enough. I also have to add:
protected $keyType = 'string';

Unable to locate factory with name on production?

I create a factory of a model inside an artisan command:
public function handle()
{
if (!$this->isDevelopment()) {
$this->errorMessageSwitchEnvToDev();
return;
}
$userId = $this->ask('Please specifiy user_id you want to add the payouts to.',2148);
$numberOfPayouts = $this->ask('How many payouts you want to generate?', 10);
factory(\App\Payout::class, $numberOfPayouts)->create([
'user_id' => $userId,
]);
}
The artisan works on my local desktop, but it does not work after deployment on my test server.
I get the following error message:
InvalidArgumentException : Unable to locate factory with name [100] [App\Payout].
at /www/htdocs/w0146a6f/dev/dev4.partner.healyworld.net/releases/20201014150056/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:269
265| */
266| protected function getRawAttributes(array $attributes = [])
267| {
268| if (! isset($this->definitions[$this->class][$this->name])) {
> 269| throw new InvalidArgumentException("Unable to locate factory with name [{$this->name}] [{$this->class}].");
270| }
271|
272| $definition = call_user_func(
273| $this->definitions[$this->class][$this->name],
Exception trace:
1 Illuminate\Database\Eloquent\FactoryBuilder::getRawAttributes([])
/www/htdocs/w0146a6f/dev/dev4.partner.healyworld.net/releases/20201014150056/vendor/laravel/framework/src/Illuminate/Database/Eloquent/FactoryBuilder.php:292
2 Illuminate\Database\Eloquent\FactoryBuilder::Illuminate\Database\Eloquent\{closure}()
/www/htdocs/w0146a6f/dev/dev4.partner.healyworld.net/releases/20201014150056/vendor/laravel/framework/src/Illuminate/Database/Eloquent/Concerns/GuardsAttributes.php:122
I do the deployment with envoyer.
My factory is defined in database/factories/PayoutFactory.php
<?php
$factory->define(\App\Payout::class, function (Faker\Generator $faker) {
return [
'user_id' => function () {
return factory(App\User::class)->create()->id;
},
'amount' => $faker->randomFloat(2),
'req_amount' => 0,
'tax_amount' => 0,
'withheld' => 0,
'vat_rate' => $faker->randomNumber(2),
'released_amount' => $faker->randomFloat(2),
'released_amount_local_currency' => $faker->randomFloat(2),
'status' => 'released',
'flag' => 0,
'created_at' => $faker->dateTimeBetween('-6 months', 'now'),
];
});
However, it won't work on production. I already cleared the cache, the routes and called composer dump-autoload, but it still failes with the same issue.
Any suggestions?
I also read all answers of Laravel 5.2: Unable to locate factory with name [default] but none of them worked.
Notice this:
Unable to locate factory with name [100]
It looks like factory() is willing to use states instead of quantity. In this case it's looking for a factory state called (string) "100" instead of (int) 100
Cast your amount variable to be an integer
$numberOfPayouts = (int) $this->ask('How many payouts you want to generate?', 10);
Alternatively, try using ->times($amount) method to be more explicit.

Laravel slack log but with additional fields and configuration

In Laravel 5.6 I'm trying to make proper slack logs and I did:
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => ['single', 'slack'],
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => 'TEST',
'icon' => ':boom:',
'level' => 'info',
],
It works but I want to specify additional fields and maybe customize it a little if it match some other conditions.
I was looking at SlackWebhookHandler.php monolog file but not all parameters work in this configuration..
For example emoji and username doesn't work - I don't know if slack already has even options for changing bot username.
Other example is that in this file something it's called useAttachment and here it's just attachment - where the names are stored..?
Back to topic I did:
Log::info('added test',['test'=>'test']);
And it works, but for slack I want to send additional field, in every request for example:
'added test',['test'=>'test', 'more' => 'test2']
How I'm able to accomplish it? I need to connect to Log Class and slack driver in some way but I don't have idea how to do this?
I debugged myself to SlackRecord::getSlackData, there you see how he handles attachments and add's additional data to the record.
For me it totally fitted to set 'context' => true in logging.php for the Slack Channel and define a Processor which just add's the Data I need to the record
class SlackProcessor {
/**
* #param array $record
* #return array
*/
public function __invoke(array $record) {
$record["context"]["Env"] = env("LOG_SLACK_USERNAME", "localhost");
$record["context"]["Full URL"] = Request::fullUrl();
$record["extra"]["Request Data"] = Request::all();
return $record;
}
}
So maybe you could just debug again to getSlackData and see why he jumps over the attachment part you need.
I was able to get closer to solution but still not at all:
On logging.php now I have
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'tap' => [App\Logging\SlackLogger::class],
'username' => 'BOT',
'attachment' => false,
'emoji' => ':boom:',
'level' => 'info',
],
I created App/Logging/SlackLogger.php:
namespace App\Logging;
use Monolog\Logger;
use Monolog\Handler\SlackWebhookHandler;
use Monolog\Formatter\LineFormatter;
use Monolog\Formatter\JsonFormatter;
class SlackLogger
{
/**
* Customize the given logger instance.
*
* #param \Illuminate\Log\Logger $logger
* #return void
*/
public function __invoke($logger)
{
$dateFormat = "Y-m-d H:i:s";
$checkLocal = env('APP_ENV');
foreach ($logger->getHandlers() as $handler) {
if ($handler instanceof SlackWebhookHandler) {
$output = "[$checkLocal]: %datetime% > %level_name% - %message% `%context% %extra%` :poop: \n";
$formatter = new LineFormatter($output, $dateFormat);
$handler->setFormatter($formatter);
$handler->pushProcessor(function ($record) {
$record['extra']['dummy'] = 'test';
return $record;
});
}
}
}
}
And It works only if I don't try to make custom attachment on slack.. When I'm trying to do:
$handler->pushProcessor(function ($record) {
$record['extra']['dummy'] = 'test';
$record['attachments'] = [
'color' => "#36a64f",
"title" => "Slack API Documentation",
"text" => "Optional text that appears within the attachment"
];
return $record;
});
the $record losts 'attachments' array.. I was checking it in SlackWebhookHandler in write function because at this pushProcessor at return it still exists, but not sending to slack. I know that can be related to $handler->setFormatter($formatter); but I if I remove It, the problem still exists - so I still don't know how to solve it.

Laravel Fractal Optional fields

I am using fractal for my small project, here is my code:
public function transform(Series $series) {
return [
'id' => $series->id,
'title' => $series->title,
'url' => $series->url,
'description' => $series->description,
'thumbnail_hd' => $series->thumbnail_hd,
'thumbnail_wide' => $series->thumbnail_wide,
'views' => $series->views
];
}
I would like to make views (which is an int) optional and not return the views unless requested - since this field is based on a relationship and will increase the processing time.
I would like to use it as relationships (so i can include particular fields whenever I need to):
// in transformer
public function includeUser(Series $series) {
return $this->item($series->user, new UserTransformer);
}
// in controller
return fractal()
->item($series)
->parseIncludes(['user'])
->transformWith(new SeriesTransformer)
->toArray();
But just for an integer instead of a whole array of data. Is it possible using Fractal?
What you can do is the following
public function transform(Series $series) {
$return = [
'id' => $series->id,
'title' => $series->title,
'url' => $series->url,
'description' => $series->description,
'thumbnail_hd' => $series->thumbnail_hd,
'thumbnail_wide' => $series->thumbnail_wide,
];
if ($series->views > 0) {
$return['views'] = (int) $series->views;
}
return $return;
}
I wouldn't suggest doing this though. I would usually just return views as 0.
If you're worried about your DB performance, your app is going to have to count the views anyway to know if they're greater than 0.
If you worried about client side performance, this is not going to matter with a single key value pair.

Laravel faker - loop to add records to simulate version control

I am using faker to seed my DB.
$factory->define(App\Product::class, function (Faker\Generator $faker) {
$campaign = factory(App\Campaign::class)->create();
$size= $faker->randomElement($array = array ('728x90','300x250','315x315', '715x425', '750x650'));
return [
'campaign_id' => $campaign->campaign_name,
'size' => $size,
'product_id' => $campaign->campaign_name."_".$size,
'version' => $faker->randomElement($array = array ('1','2','3', '4', '5')),
];
});
The bit I am interested in is the version field. What I would like to do is generate a random number between 1 and 5 and then enter that number of records in the database,
So a product can have been 1 and 5 entries depending on the number of 'versions' which have bene created.
Is this possible?
I tried a simple for loop around the return array with no luck.
From what you say, you want to create multiple entries for the same product with different versions. The Model factory can be used to create a single model entry. You can use the faker directly in the seeder and achieve what you are expecting.
$campaign = factory(App\Campaign::class)->create();
$size= $faker->randomElement($array = array ('728x90','300x250','315x315', '715x425', '750x650'))
$max_version = $faker->randomElement($array = array ('1','2','3', '4', '5'));
for ($version=0; $version < $max_version; $version++) {
Product::create([
'campaign_id' => $campaign->campaign_name,
'size' => $size,
'product_id' => $campaign->campaign_name."_".$size,
'version' => $version,
]);
}
One of the simplest solutions is to create factory method (or trait) in your test class, something like this, you'll get the point :)
public function createProduct()
{
$product = factory(Product::class)->create();
foreach(range(0, $product->version) as $i) {
factory(Version::class)->create([
'product_id' => $product->id
]);
}
return $product;
}

Categories