Data queried from the database.
year 2021 = [
{
"month": 1,
"total": "1,482"
},
{
"month": 2,
"total": "5,422"
},
]
and
year 2020 = [
{
"month": 1,
"total": "2,482"
},
{
"month": 2,
"total": "6,422"
},
{
"month": 3,
"total": "7,422"
},
.........
{
"month": 12,
"total": "20,422"
},
]
Here I create a method called `GetData(), I create a loop from 1-12 based on the months in a year. Next I want to combine data in an array, but how do I combine the data?
Below is my controller model which calls the same model method over and over with different parameters:
public function GetData(){
$thn_now = 2021;
$thn_before = 2020;
$bulan = array();
for ($bul = 1; $bul <= 12; $bul++) {
$sql = $this->My_model->model_month($bul,$thn_now)->result(); // Here I make the current year parameter
// $sql2 = $this->My_model->model_month($bul,$thn_before)->result(); //here are the parameters of the previous year
foreach($sql as $row) {
$bulan[] = array(
'bulan' => $bul,
'total_now' => $row->hasil,
// how to display the previous year's total in an array ?
// 'total_before' => $row->hasil,
);
}
}
$this->set_response($bulan,200);
}
I want an output like this:
[
{
"month": 1,
"total_now": "1,482"
"total_before": "2,482"
},
{
"month": 2,
"total_now": "5,522"
"total_before": "6,422"
},
{
"month": 3,
"total_now": null
"total_before": "7,422"
},
.........
{
"month": 12,
"total_now": null,
"total_before": "20,422"
},
]
In 2021, the total is only until the month of 02, while in 2020 the total is until month 12.
If 2021 is only up to month 02 then the next array is total null.
I don't recommend that you make 24 trips to the database to collect individual totals; I'll urge you to create a new model method which delivers all of the monthly totals in a single trip.
I don't recommend allowing your controller to dictate the type of result set that is created from the active record in the model. The model should be consistently returning the same data in the same structure throughout your application.
It seems to me that your inner loop is only collecting one row anyhow, so instead of result() which creates an array of objects, just call row() which creates null or an object then immediately access the hasil property. Using the null coalescing operator, you can fall back to null when hasil is not a property of non-object null.
Professional programmers around the world (including ones that don't use English as their first language will recommend writing variables using English words. This allows a more consistent language for other developers to review and maintain your code. After all, you are using English in the rest of your php syntax, just keep it all in English.
For future flexibility, allow the "current year" to be passed in as a controller method argument. This will allow you to seamlessly view historic data (should the need ever arise). If there is no passed in argument, then default to the current year.
Hard coding the number of each month is acceptable because it will never change. However, as a general rule, try to avoid using hardcoded numbers/values in your script so that it is more flexible, easier to maintain, and more intuitive to read.
Suggested code (tolerating that you are passing up to 24 active record objects back to the controller):
public function GetMonthlyTotalsSincePreviousYear($year = null): void
{
$thisYear = $year ?? date('Y');
$lastYear = $thisYear - 1;
$result = [];
for ($month = 1; $month <= 12; ++$month) {
$result[] = [
'month' => $month,
'total_now' => $this->My_model->model_month($month, $thisYear)->row()->hasil ?? null,
'total_before' => $this->My_model->model_month($month, $lastYear)->row()->hasil ?? null,
];
}
$this->set_response($result, 200);
}
In my own application, I would have a model method that collects all of the data and a controller like this:
public function getMonthlyTotalsSincePreviousYear(int $year = null): void
{
$this->set_response(
$this->My_model->getMonthlyTotalsSincePreviousYear($year ?? date('Y')),
200
);
}
This, I believe, will assist you in developing the solution. I haven't included the entire array of $year_2020, but I hope you get the idea -
$year_2021 = [
[
"month" => 1,
"total_now" => "1,482"
],
[
"month" => 2,
"total_now" => "5,422"
]
];
$year_2020 = [
[
"month" => 1,
"total_before" => "2,482"
],
[
"month" => 2,
"total_before" => "6,422"
],
[
"month" => 3,
"total_before" => "7,422"
]
];
$output = [];
foreach ($year_2021 as $obj) {
$key = $obj['month'];
$output[$key] = $obj;
}
foreach ($year_2020 as $obj) {
$key = $obj['month'];
if (isset($output[$key])) {
$output[$key]['total_before'] = $obj['total_before'];
} else {
$obj['total_now'] = null;
$output[$key] = $obj;
}
}
$output = array_values($output);
error_log(json_encode($output));
Output
[{"month":1,"total_now":"1,482","total_before":"2,482"},{"month":2,"total_now":"5,422","total_before":"6,422"},{"month":3,"total_before":"7,422","total_now":null}]
Related
When i tried to change the value of id to encrypted Id is using map function and transform function, it get zero value no string value or alpha numeric getting replaced .
but it is mandatory to encrypt all id's for API
Please Help me .
Function
function getLatestArticle($profileId)
{
$data = Article::wherehas('articleTrans',function($query){
$query->where('status', ApiConstants::PUBLISHED_STATUS);
})->with(['articleTrans'=>function($q){
$q->select('id','article_id','language_id','title','short_description','description','main_image');
$q->where('status', ApiConstants::PUBLISHED_STATUS);
}])->latest()->paginate(ApiConstants::D_PAGE_C);
$data->getCollection()->transform(function ($value) {
$value->id = encrypt($value->id);
return $value;
});
return $data;
}
Collection
"latest_data": [
{
"id": 0,
"profile_id": 3,
"name": "Test",
"trending": 0,
"new_arrived": 0,
"featured": 0,
"reading_time": null,
"has_series": 0,
"status": 0,
"created_by": 1,
"updated_by": 1,
"deleted_at": null,
"created_at": "2022-03-24T10:27:16.000000Z",
"updated_at": "2022-03-31T11:41:14.000000Z",
"created_from": 1,
"article_trans": {
"id": 8,
"article_id": 12,
"language_id": 1,
"title": "Test",
"short_description": "Test",
"description": "<p><strong>TestTestTestTestTestTestTest </strong></p>\r\n\r\n<p><strong>TestTestTestTestTestTestTestTestTestTestTestTestTest</strong></p>",
"main_image": "1648117636_AXwwVY6JSmNTxmXIiRqGlXiePTl70chCkmMDlehp.jpeg",
"image_url": "http://127.0.0.1:8000/storage/admin/article/image/1648117636_AXwwVY6JSmNTxmXIiRqGlXiePTl70chCkmMDlehp.jpeg"
}
}
Its not good solution to temporarily modify entity objects only for change API response format.
Better solution is using DTO/Transformer classes. Good example of transformers implementation is https://fractal.thephpleague.com/transformers/
In this way you can split presentation and model.
Using any packages is optional, you can write simple transformer classes like this:
// app/Transformers/ArticleTransformer.php
final class ArticleTransformer
{
public static function transform(Article $article): array
{
return [
'id' => encrypt($article->id),
// any fields
];
/*
// or if you need change only one property
return array_replace(
$article->toArray(), // or $article->jsonSerialize()
[
'id' => encrypt($article->id),
],
);
*/
}
public static function transformCollection(Collection $articles): Collection
{
return $collection->map(fn (Article $article): array => self::transform($article));
}
}
Then use this transformer:
return [
'latest_data' => ArticleTransformer::transformCollection($latestArticles),
];
Not very good, but should work:
Override jsonSerialize/toArray method of model, in returning modify your id.
I have the following data structure. I need an array of movies. Each movie has a title, rating and year.
Movies:
--Title = "The Hobbit";
--Rating = 7;
--Year = 2012;
--Title = "Lord of the rings";
--Rating = 5;
--Year = 2001;
If this was JavaScript you would have an array of objects:
const movies = [{
title:"The Hobbit",
rating:7,
year:2012
},
{
title:"Lord of the rings",
rating:5,
year:2001
}]
How do you model this in PHP? I know that you could create a class of Movie and each movie would be an instance of this class, but is this required? Can you just have non-class objects like with JavaScript?
There are two ways:
Associative arrays:
$movies = [
[
"title" => "The Hobbit",
"rating" => 7,
"year" => 2012
],
[
"title" => "Lord of the rings",
"rating" => 5,
"year" => 2001
]
];
Or you use an object of type \stdClass.
Easiest definition:
$movie1 = (object)[
"title" => "The Hobbit",
"rating" => 7,
"year" => 2012
];
Or you do it this way:
$movie1 = new \stdClass();
$movie1->title = "The Hobbit";
Access works like that:
echo $movie1->title; // The Hobbit
You can collect them again in $movies:
$movies = [$movie1];
You absolutely can! Objects extend a base \stdClass which would behave similarly to your description. A great example of this is the json_decode() method defaults.
To create one of these objects from scratch, either invoke the \stdClass constructor, or just typecast an array to (object).
Based on your example JSON, here's the use of a \stdClass in PHP, both from serialisation and instantiating your own objects.
<?php
$decoded = json_decode('[{
"title": "The Hobbit",
"rating": 7,
"year": 2012
},
{
"title": "Lord of the rings",
"rating": 5,
"year": 2001
}
]');
print_r($decoded);
echo $decoded[0]->title . PHP_EOL;
print_r(new \stdClass());
print_r((object)[]);
print_r((object)['example_public_property' => 'example_value']);
For example, we have a songs table, and a favorites (songs) table.
If I use Songs::with('favorites')->get(), it will return as follows:
"songs": [
{
"id": 43,
"name": "Song 1",
"favorites": [
{
"id": 52,
"user_id": 25,
"song_id": 43
}
]
},
{
"id": 44,
"name": "Song 2",
"favorites": []
},
What I want to do is, in case the song has been favorited, return 1, and if not, return 0, as follows:
{
"id": 43,
"name": "Song 1",
"favorites": true (or 1)
},
{
"id": 44,
"name": "Song 2",
"favorites": false (or 0)
},
Is there a way to do it without having to run through the returned collection array manually in PHP?
I think this is the simplest solution using Eloquent
Song::withCount([
'favorites' => function ($query) {
$query->select(DB::raw('IF(count(*) > 0, 1, 0)'));
}
])->orderBy('favorites_count', 'desc');
There are a bunch of different ways to do what you want, it all depends on what works best for you.
If you want the result returned in your result set, you can setup a scope:
class Song extends Model
{
public function scopeAddFavorite($query, $userId = null)
{
$andUser = !empty($userId) ? ' AND favorites.user_id = '.$userId : '';
return $query->addSelect(\DB::raw('(EXISTS (SELECT * FROM favorites WHERE favorites.song_id = songs.id'.$andUser.')) as is_favorite'));
}
}
Since this scope modifies the 'select' statement, you need to make sure to manually specify the other columns you want before adding in the scope.
$songs = Song::select('*')->addFavorite()->get();
I added in the ability to pass in the user id, in case you wanted to specify that the column only return true if the song has been favorited by a specific user.
$songs = Song::select('*')->addFavorite(25)->get();
Another option, you can add an accessor to your model that will handle the check for you. You can read about accessors here.
class Song extends Model
{
// only do this if you want to include is_favorite in your json output by default
protected $appends = ['is_favorite'];
public function getIsFavoriteAttribute()
{
// if you always want to hit the database:
return $this->favorites()->count() > 0;
// if you're okay using any pre-loaded relationship
// will load the relationship if it doesn't exist
return $this->favorites->count() > 0;
}
}
Usage:
$song = Song::find(1);
// access is_favorite like a normal attribute
var_dump($song->is_favorite);
// with $appends, will show is_favorite;
// without $appends, will not show is_favorite
var_dump($song);
Using a raw SQL approach
DB::select('SELECT s.*, (CASE WHEN f.id IS NULL THEN 0 ELSE 1 END) as favorites FROM songs s LEFT JOIN favorites f ON s.id = f.song_id GROUP BY s.id')
will return the following structure:
[
StdClass { 'id' => 1, 'name' => 'Song 1', 'favorites' => 1 },
StdClass { 'id' => 2, 'name' => 'Song 2', 'favorites' => 0 },
StdClass { 'id' => 3, 'name' => 'Song 3', 'favorites' => 1 },
]
Now, using Eloquent
Song::withCount('favorites')->get()
will return the an array of objects of the Song class
[
Song { 'id' => 1, 'name' => 'Song 1', 'favorites_count' => 1 },
Song { 'id' => 2, 'name' => 'Song 2', 'favorites_count' => 0 },
Song { 'id' => 3, 'name' => 'Song 3', 'favorites_count' => 3 }
]
The difference is the first will return an array of PHP standard objects while the second an array of Song objects and the first is faster than second.
You can't do something like this without somehow manipulating the original results object first.
You don't really specify how you need to use or iterate through the data, but maybe Query Scopes can help you? Either way you'll need to iterate over the data once to manipulate it. A high order function like map will help you do this.
suppose you have the $songs array, you can do this,
foreach($songs as $song)
$song["favorites"] = is_null($song["favorites"]) ? true : false;
Performing a query that simulates a 'like/mysql' searching for teams on the name of the team
Team document structure
{
"_id": 9,
"name": "azerty",
"tag": "dsfds",
"desc": "ggdfgsdfgdfgdf",
"captain": 8,
"coach": 8,
"members": [{
"date_joined": "2016-03-31 15:22:09",
"user_id": 8
}, {
"date_joined": "2016-03-31 19:22:35",
"user_id": 9
}],
"current_invites": [{
"invite_id": 21,
"username": "Nikki",
"user_id": "9",
"status": 1,
"date_invited": "2016-03-31 18:32:40"
}, {
"invite_id": 22,
"username": "Nikki",
"user_id": "9",
"status": 2,
"date_invited": "2016-03-31 18:33:16"
}]
}
PHP Code =
$q = '/.*'.$q.'*./';
$result = $this->coll->aggregate(
array('$match' => array('name' => $q)),
array('$project' => array('name' => 1,'members' => array('$size' => '$members'))));
Feels like I'm going mad not knowing how to fix this.
Have used regex before after migrating to mongo but not with the combination of agg-match.
in my case i am finding the aggregated result in which you can not set the where clause so use the aggregated functions like $sort $unwind $orderby and so on i am using the all of the above mention and have the problem with the like stuff to match the string like %str% here my code in which i implement the like using $match with MongoRegex
public function getRecords($table,$where = array(),$like_key = false,$like_value = false,$offset = 1,$limit = 10,$order_column = false,$order_type = false, $isAggregate=false,$pipeline=array()){
$temp = $this->getMongoDb()->where($where);
if($like_key && $like_value){
$temp = $temp->like($like_key,$like_value);
// this like filter is for aggregated result work both on normal get record or by aggregated result
$pipeline[]=array(
'$match' => array( $like_key => new MongoRegex( "/$like_value/i" ) )
);
}
if($order_column && $order_type){
$order_by = array();
$order_by[$order_column] = $order_type;
$temp = $temp->order_by($order_by);
$pipeline[]=array(
'$sort'=>array($order_column => ($order_type =="desc")? -1 : 1)
);
}
I got the solution when I read the following aggregation framework go the aggregation framework
I hope you will get your solution to resolve your issue.
I'm using PHP with MongoDB, How can apply below commend inside?
db.event.group({
keyf: function(doc) {
return {
year: doc.created.getFullYear(),
month: doc.created.getMonth() + 1,
day: doc.created.getDate()
}
},
reduce: function(curr, result){
result.count++;
},
initial: {count: 0}
});
I have tried below, but NOT working. Looks like not supprt keyf?
$keyf = 'function(doc){return {year: doc.created.getFullYear(), month: doc.created.getMonth()+1, day: doc.created.getDate()}}';
$initial = array('count' => 0);
$reduce = 'function(curr, result){result.count++;}';
$collection->group($keyf, $initial, $reduce);
It looks like you are basically counting the amount of documents under a date.
It should be noted that the group command has numerous flaws including:
Not officially supporting sharding (warning not to use it)
Is basically JavaScript
Is Basically a Map Reduce
Is extremely slow
that means it has since been "deprecated" in favour of the aggregation framework, which in PHP for you would be:
$db->collection->aggregate(array(
array('$group' => array(
'_id' => array(
'day' => array('$dayOfMonth' => '$created'),
'month' => array('$month' => '$created'),
'year' => array('$year' => '$created')
),
'count' => array('$sum' => 1)
))
));
To understand what operators I used etc you can look here:
http://docs.mongodb.org/manual/reference/operator/aggregation/dayOfMonth/
http://docs.mongodb.org/manual/reference/operator/aggregation/month/#exp._S_month
http://docs.mongodb.org/manual/reference/operator/aggregation/year/
http://docs.mongodb.org/manual/reference/operator/aggregation/sum/
The PHP driver does have the MongoCode class for constructing the JavaScript values that are required.
But you are actually better off using the .aggregate() command to this as it is "native* code and does not rely on the JavaScript engine. So it is much faster at producing results.
db.collection.aggregate([
{ "$group": {
"_id": {
"year": { "$year": "$created" },
"month": { "$month": "$created" },
"day": { "$dayOfMonth": "$created" }
},
"count": { "$sum": 1 }
}}
])
Data Problem
So the aggregate function works are expected, but you seem to have a problem with your test data. Here is cwhat you gave:
db.post.insert({'field':'b', 'created':new Date('2014, 1, 1')});
db.post.insert({'field':'c', 'created':new Date('2014, 1, 1 11:11:11')});
db.post.insert({'field':'d', 'created':new Date('2014, 1, 1 12:00:00')});
db.post.insert({'field':'a', 'created':new Date('2014, 1, 2')});
db.post.insert({'field':'b', 'created':new Date('2014, 1, 2')})
And this produces the data:
{ "field" : "a", "created" : ISODate("2013-12-31T13:00:00Z") }
{ "field" : "b", "created" : ISODate("2013-12-31T13:00:00Z") }
{ "field" : "c", "created" : ISODate("2014-01-01T00:11:11Z") }
{ "field" : "d", "created" : ISODate("2014-01-01T01:00:00Z") }
{ "field" : "a", "created" : ISODate("2014-01-01T13:00:00Z") }
{ "field" : "b", "created" : ISODate("2014-01-01T13:00:00Z") }
So it looks like you were trying to add "hours" in the same day to test the grouping. But the arguments to Date() are not correct. You wanted this:
db.post.insert({'field':'b', 'created':new Date('2014-01-01')});
db.post.insert({'field':'c', 'created':new Date('2014-01-01 11:11:11')});
So the whole date as a string and not the "comma" separated values