Symfony- passing an array of ids - php

I wrote an api with a function that sets notification as read by passing it's id.
But also, there should be an option to pass array of ids there, to mark several at once as read. I should extend function so that it handles the case where $this>data['id'] is an array.
Is this the right way?
My Service:
public function read($id = []){
$notification = $this->getRepository()->findBy([
'id' => $id
]);
if($notification) {
$notification[0]->setRead(new \DateTime());
$this->em->flush();
}
}
My Controller:
public function readAction()
{
$this->requirePostParams(['id']);
$this->get('app')->read(
$this->data['id']
);
return $this->success();
}

You can indeed pass an array of id values to \Doctrine\ORM\EntityRepository::findBy(); e.g:
$notifications = $this->getRepository()->findBy([
'id' => [1, 2, 3] // etc.
]);
However, since findBy() can return multiple results, it will return an array (or array-like object like Doctrine\ORM\PersistentCollection). Therefore you should iterate over your result set:
foreach ($notifications as $notification) {
$notification->setRead(new \DateTime());
}
$this->em->flush();
Additionally, it's a matter of taste to some degree but you may want to make your API more explicit and create separate methods for a single action versus a group action; e.g:
public function read(int $id)
{
//in this scenario you are searching for one notification
// only so you can use `findOneBy()` instead
$notification = $this->getRepository()->findOneBy(['id' => $id]);
$notification->setRead(new \DateTime());
$this->em->flush();
}
public function readMany(array $ids)
{
$notification = $this->getRepository()->findBy(['id' => $ids]);
foreach ($notifications as $notification) {
$notification->setRead(new \DateTime());
}
$this->em->flush();
}
As pointed out by #Yoshi, read() could also be neatly implemented as:
public function read(int $id)
{
$this->readMany([$id]);
}
Hope this helps :)

Related

Laravel - Shared data reduce number of queries

I am trying to make a global search feature on my website, and for that I need to query the database to get the records. The frontend of my application expect the following array:
[
['name' => 'Result #1...', 'url' => 'http://...'],
['name' => 'Result #2...', 'url' => 'http://...'],
['name' => 'Result #3...', 'url' => 'http://...'],
]
To accomplish this, I have added the below in my AppServiceProvider:
public function boot()
{
view()->composer('*', function($view) use ($auth) {
return \View::share('SearchData', (new GlobalSearch($auth->user()->currentTeam))->all());
});
}
The $SearchData is created in the GlobalSearch class:
class GlobalSearch
{
public $data;
public function __construct(public Team $team){
$this->data = $team->properties()->with(['leases', 'leases.tenant', 'leases.files', 'leases.invoices']);
}
protected function propertyData() : array
{
$properties = $this->data->get();
return $properties->map(function ($property) {
$array['name'] = \Str::limit($property->address, 40);
$array['url'] = route('properties.show', ['property' => $property]);
return $array;
})->toArray();
}
public function all() : array
{
return $this->propertyData();
}
}
Now the above code does work - I successfully get an array in the correct mapping. However, in my database I only have 1 property in the properties table - yet, there are being executed 90 duplicate queries for a single page load.
Why is this happening? I can't seem to locate why these queries are being duplicated
You can actually remove the view()->composer('*', function) part. Since View::share() does the exact same.
$searchData = (new GlobalSearch($auth->user()->currentTeam))->all();
View::share('SearchData', $searchData);
Optionally:
You could bind the GlobalSearch class to the container in your AppServiceProvider. Which will make the class available via app(GlobalSearch::class) wherever you want in the application. This means the query will only run once during the initialization process and maintain the data.
More info about singleton bindings: https://laravel.com/docs/8.x/container#binding-a-singleton
$this->app->singleton(GlobalSearch::class, function ($app) {
return new GlobalSearch(auth()->user()->team);
});

Laravel all() method on Model returning null within Collection

I am not sure, what is going on here. I have a collection of Model ID's but want to fallback on using all if specific ID's are omitted.
So I have this code:
use App\Models\Post;
function list($ids = [])
{
$posts = collect($ids)->whenEmpty(function ($posts) {
return Post::all()->pluck('id');
})->each(function ($item, $key) {
$post = Post::findOrFail($item);
});
}
Works fine if I pass in specific IDs via $ids. But when I leave it blank Post::all()->pluck('id'); inside of whenEmpty() returns empty. But if I call Post::all()->pluck('id'); outside the collection it works just fine. So I thought it might be some sort of scoping issue since its inside a closure, but changing it to:
use App\Models\Post;
function list($ids = [])
{
$posts = collect($ids)->whenEmpty(function ($posts) {
return \App\Models\Post::all()->pluck('id');
})->each(function ($item, $key) {
dd($item);
});
}
Is still showing up as "" If I dd() the whole collection its just:
[
0 => ""
]
So even providing the whole namespace isn't working. What am I missing here?
Here it's one approach more
function list(array $ids = [])
{
if(empty($ids)) $posts = Post::all();
else $posts = collect($ids)->map(function($id) {
return Post::findOrFail($id);
});
$posts->each(...); /* collection */
}
if you want to use whenEmpty()
function list(array $ids = [])
{
collect($ids)->map(function($id) {
return Post::findOrFail($id);
})->whenEmpty(function($posts){
return $posts = Post::all();
})->each(...);
}
I know this might not directly answer your question (because I would do this in a different way) but maybe it's helpful for you or others in the same situation.
What I would do is using a Request class to validate the introduced IDs like this:
use Illuminate\Validation\Rules\Exists;
class PostsRequests extends FormRequest
{
/**
* Get the validation rules that apply to the request.
*
* #return array
*/
public function rules()
{
return [
'ids' => ['array'],
'ids.*' => ['numeric', new Exists('posts','id')],
];
}
/**
* Handle a passed validation attempt.
*
* #return void
*/
public function passedValidation()
{
$this->ids = $this->validated()['ids'];
}
}
This way you make sure all the IDs introduced in the array exist in the posts table. Otherwise, the request fails.
Now that you know all the IDs are valid, you can simply check if the array is empty or not:
class PostController extends Controller
{
public function list(PostsRequests $request)
{
$posts = empty($request->ids) ? Post::all() : Post::whereIn('id', $request->ids])->get();
}
}
UPDATE
Since the array of IDs is not coming from a request, you can use a Validator in the controller itself:
use Illuminate\Validation\Rules\Exists;
use Illuminate\Support\Facades\Validator;
class PostController extends Controller
{
public function list(array $ids)
{
Validator::make($ids, [
'*' => ['numeric', new Exists('posts','id')],
])->validate();
$posts = empty($ids) ? Post::all() : Post::whereIn('id', $ids])->get();
}
}

Having difficulties displaying all content on my wishlist table

I am trying to retrieve the data on my wishlist table, for a particular user, so far it only retrieves the first data on the table, just returning one array instead of the three in the table with same user id
public function getWishlistByUserId($id){
$wishlists = Wishlist::where('userId', $id)->get();
foreach($wishlists as $wishlist){
$products = Product::where('id', $wishlist->productId)->get();
return $products;
}
}
It happens because the foreach loop returns a value during the first iteration. Place your return statement outside the loop. Also you could improve your performence by making use of relationships.
An example could be:
// Product.php
public function wishlists()
{
return $this->hasMany(Wishlist::class);
}
// Your method
public function getWishlistByUserId($id)
{
return Product::whereHas('wishlists', function ($query) use ($id) {
$query->where('userId', $id);
});
}
Ideally this is n+1 situation
So i will suggest to use laravel relationship like:
in your whishlist model
public function product(){
return $this->hasMany(Product::class,'productId','id');
}
get data with relationship
public function getWishlistByUserId($id){
$wishlists = Wishlist::with('product')->where('userId', $id)->get();
}
I was finally able to get it working this way, i just pushed the result into an array, and then returned it outside the loop, thanks everyone for your help
public function getWishlistByUserId($id){
$wishlists = Wishlist::where('userId', $id)->get();
$wishlist = [];
foreach($wishlists as $wish){
$product = Product::where('id', $wish->productId)->get();
array_push($wishlist, $product);
}
return $wishlist;
}

Laravel mass assignment to model in Update method in abstract class using foreach loops to fill keys with values

How can I be able to create a generic method of the update() function that belongs to an abstract class. To be precise I want it to be able to generate the array $keys and link with the updated $values dynamically, so that different repositories can use this function from this abstract class.
public function update($id, array $data)
{
$related = $this->modelClassInstance->find($id);
$related->name = $data['name'];
$related->description = $data['description'];
$related->started_at = $data['started_at'];
$related->ended_at = $data['ended_at'];
if($related->save()) {
$response = MyResponse::error(201, true, 'Event sucessfully updated');
return $response;
}
else {
$response = MyResponse::error(200, false, 'Event unsucessfully updated');
return $response;
}
}
I need something like this
$data = array(1,2,3,4,5,6,7,8,9);
foreach($data as $key=>$value)
{
.....
}
My keys are the name, description, started_at and ended_at, but that means this function is specific to a model . I want this keys to be generated dynamicaly and to get their new values from the data passed. So that any model with any keys can use it.
The for loop was unneccesary with Laravel methods which are quite reliable, after doing research in various sites. I came up with this simple solution
public function update($id, array $data)
{
$related = $this->modelClassInstance->find($id);
$related->fill($data);
if($related->save())
{
$response = MyResponse::error(201, true, 'Event sucessfully updated');
return $response;
}
else
{
$response = MyResponse::error(200, false, 'Event unsucessfully updated');
return $response;
}
}
This is how the fill() method is implemented in Laravel Eloquent Model, in case you seek similar implementation.
public function fill(array $attributes)
{
$totallyGuarded = $this->totallyGuarded();
foreach ($this->fillableFromArray($attributes) as $key => $value)
{
$key = $this->removeTableFromKey($key);
// The developers may choose to place some attributes in the "fillable"
// array, which means only those attributes may be set through mass
// assignment to the model, and all others will just be ignored.
if ($this->isFillable($key))
{
$this->setAttribute($key, $value);
}
elseif ($totallyGuarded)
{
throw new MassAssignmentException($key);
}
}
return $this;
}

symfony2 Doctrine delete an array of objects

I would like to delete all the records from database matching a particular user_id in Symfony2.
$em = $this->getDoctrine()->getManager();
$user_service = $em->getRepository('ProjectTestBundle:UserService')
->findByUser($this->getUser()->getId());
This might return a few matching objects, so when I run:
$em->remove($user_service);
$em->flush();
an error occurs:
EntityManager#remove() expects parameter 1 to be an entity object, array given.
How do I remove all records (objects) matching a particular condition?
Btw, when I run an equivalent sql statement in mysql, it works perfectly.
Why don't you just loop through the objects array?
$user_services = $em->getRepository('ProjectTestBundle:UserService')
->findByUser($this->getUser()->getId());
foreach ($user_services as $user_service) {
$em->remove($user_service);
}
$em->flush();
You could also use something like this:
$user_services = $em->getRepository('ProjectTestBundle:UserService')->findByUser($this->getUser()->getId());
array_walk($user_services, array($this, 'deleteEntity'), $em);
$em->flush();
Then add this method in your controller:
protected function deleteEntity($entity, $key, $em)
{
$em->remove(entity);
}
Or simply use:
$user_services = $em->getRepository('ProjectTestBundle:UserService')->findByUser($this->getUser()->getId());
$this->deleteEntities($em, $user_services);
$em->flush();
...
protected function deleteEntities($em, $entities)
{
foreach ($entities as $entity) {
$em->remove($entity);
}
}
Note that when using Propel and the PropelBundle, the PropelObjectCollection implements a delete() function so you don't have to do this loop by hand.
You can also make use of doctrine query builder delete method.
public function deleteAllByUser(UserInterface $user)
{
$query = $this->createQueryBuilder('related_entity')
->delete()
->andWhere('related_entity.user = :user')
->setParameter('user', $user)
->getQuery();
return $query->execute();
}
If you return one object, you just need to write:
->findOneByUser,
You won't need a foreach loop. If your return is an array, then you need to do ->findByUser and write a for loop:
foreach ($entities as $entity)
{
//do something
}

Categories