I have a small fundraiser portion of my site and I need to keep adding to the total amount on every transaction and how many sponsors I get.
I have my transaction set as a POST request.
Route::post('/fundraiser/checkout', 'FundController#fundCheckout')->name('fundraiser-checkout');
In my controller I'm doing the following to increment the sponsors_received and funding_received.
Note the below $subscription->quantity = the amount given. $45 * 100 = 4500 cents for stripe.
$funds = DB::table('funds')->where('cin', $cin)
->increment('funding_received', $subscription->quantity)
->increment('sponsors_received');
Want to keep adding to funding_received total and sponsors_received.
This actually does add to my funding received, but fails on sponsors_received with an odd error.
Symfony\Component\Debug\Exception\FatalThrowableError Call to a member
function increment() on integer
If I remove the funding_received query everything works fine. (Can I not use increment twice in a query?)
$funds = DB::table('funds')->where('cin', $cin)
->increment('sponsors_received');
Is there another way I can add to funding_received other than using increment?
Using Laravel 6+
The increment method from the Query Builder is basically a custom update call that handles the increment process. And because it is an update, it returns the same result as an update call, which is the number of updated entries. That's why you get the error:
Call to a member function increment() on integer
So you can't chain the next increment statement because the first one returns a number (integer) instead of a Query Builder instance. You can however do something like this:
$query = DB::table('funds')->where('cin', $cin);
$query->increment('funding_received', $subscription->quantity)
$query->increment('sponsors_received');
This will create a base query with your given condition and run each increment individually.
You're correct. The return type of public function increment() is an int https://laravel.com/api/6.x/Illuminate/Database/Eloquent/Builder.html#method_increment, so chaining calls to increment() is not allowed.
int increment(string $column, float|int $amount = 1, array $extra = [])
Increment a column's value by a given amount.
Parameters
string $column
float|int $amount
array $extra
Return Value
int
A simple way to update this is using the save() method:
$funds = Funds::where('cin', $cin)->firstOrFail();
$funds->funding_received += $subscription->quantity;
$funds->sponsors_received++;
$funds->save();
Note: This assumes you have a model for your funds table.
Related
I have an array of student objects. Each student has subjects as an array collection. Each subject has a function getStatus() that is calculated based on different arguments etc, thus there is no real property for status in the subject entity.
How can I count the number of subjects that are completed, in progress, and pending and display it per student in a table?
I could retrieve my students in my controller like this:
$students = $em->getRepository(Student::class)->findAll();
and then perhaps with loops count it somehow, but I don't get how.
I thought of creating a function that implements the filter on the array collection like seen in this answer, but I don't understand how to use that to filter on getStatus().
I also thought to implement ->matching with a criteria like this:
public function getSubjectsByStatus(string $status): ?Collection
{
$subjects = $this->subjects->matching(
Criteria::create()->where(Criteria::expr()->eq($this->getStatus(), $status))
);
return $subjects ?? null;
}
and then do a count on the returned collection, but the first parameter of eq() should be a string and I don't have a status property in the subject entity that can be used as a string, and adding a property now is not a good idea.
How can I count all subjects, pending subjects, completed subjects, and in progess subjects the best way?
You probably should consider making your status value an actual field in your database, since your problem would be easy to solve with a SQL/DQL query.
Without that being the case, here is how you could implement your getSubjectsByStatus method:
public function getSubjectsByStatus(string $status): ?Collection
{
return $this->tl1Configs->filter(function ($element) use ($status) {
return $element->getStatus() == $status;
});
}
But if you call that method three times to just count the amount of all status values, you are looping over your collection three times as well.
A "better" solution would probably be to make a specialized method to explicitly get these counts. This is just one way of achieving what you want though. A method to return an array of sub-collections instead of just status counts could is another solution if you want to actual work with the sub-collections (all depends on your actual usecase).
public function getSubjectStatusCounts(): array
{
$statusCounts = [];
foreach ($this->tl1Configs as $subject) {
$statusCounts[$subject->getStatus()] = ($statusCounts[$subject->getStatus()] ?? 0) + 1;
}
return $statusCounts;
}
I have a table representing events, each of which has a notice period, e.g. you can't book the event if it's currently less than 24 hours before the event.
I'm trying to create a 'bookable' scope for this, but am failing. Specifically, in the below, 'time' represents the time of the event (timestamp), and 'notice' the notice period, in minutes (integer), both of which are columns in the Events model. What I've found is that Laravel is not reading the 'notice' variable, i.e. treating it as 0. Any guidance would be appreciated, thanks.
public function scopeBookable($q) {
$q->where('time','>',Carbon::now()->addMinutes('notice'))->orderBy('time','ASC')->get();
}
The addMinutes() method expects an integer not a string.
Scope Option
You can pass the notice time through to the scope.
// Controller
$notice = 60;
Events::bookable($notice);
// Model
public function scopeBookable($q, $notice=0) {
$q->where('time','>',Carbon::now()->addMinutes($notice))->orderBy('time','ASC')-get();
}
Collection Option
You can always execute a self-join in SQL and check the value of notice in a subquery. Another option is to return a filtered eloquent collection.
public function scopeBookable() {
return Events::all()->filter(function($event) {
return $event->time > Carbon::now()->addMinutes($event->notice)
});
}
I am working in a legacy system that uses row() plus limit() to get one result. I didn't understand why, because row() already give me one result, but a coworker said that improves performance. Example:
$this->db
->select()
->select('extract(epoch from cadevolucao.dt_sistema) as data_sistema')
->select('extract(epoch from cadevolucao.dt_previsao_alta) as data_previsao')
->select('cadevolucao.cd_evolucao, cadevolucao.dt_sistema')
->join('contatnd', 'cadevolucao.num_atend = contatnd.num_atend')
->join('cadplanejamento', 'cadevolucao.cd_evolucao = cadplanejamento.cd_evolucao')
->where('contatnd.cd_pessoa', $cd_pessoa)
->where('tp_evolucao', -1)
->where('tipo', 1)
->order_by('cadevolucao.cd_evolucao','desc')
->limit(3)
->get('cadevolucao')
->row();
I looked for in the CI Documentation and Google, not founding anything useful about that.
Can someone explain if it's needed the limit() when using row() in Active Record's CI and why?
According to what i know row method returns a single result row. If your query has more than one row, it returns only the first row.But internally its still fetching all the rows fetched by the query and storing it in an array. Yes i think i must agree with your co-worker indeed limit will have a performance impact.
this is what row method does internally
/**
* Returns a single result row - object version
*
* #param int $n
* #return object
*/
public function row_object($n = 0)
{
$result = $this->result_object();
if (count($result) === 0)
{
return NULL;
}
if ($n !== $this->current_row && isset($result[$n]))
{
$this->current_row = $n;
}
return $result[$this->current_row];
}
as you its either returning the first element or the argument supplied i.e the row index.
row is actually an alias to this row_object
I need to get the sum of the field "valor", from the table "orcamentos".
I'm using this and it is working, but I know that this is not the right way:
//function index() from OrcamentosController.php
$orcamentoSubprojetosTotal = $this->Orcamento->query(
"SELECT
SUM(Orcamento.valor) AS TotalOrcamentoSuprojetos
FROM
orcamentos AS Orcamento
WHERE
Orcamento.subprojeto_id IS NOT NULL;"
);
$this->set(compact('orcamentoSubprojetosTotal'));
I have found this question cakephp sum() on single field (and others sum() function in cakephp query, using virtual fields to sum values in cakephp), but in the moment I add this line to my controller:
$this->Orcamento->virtualFields['total'] = 'SUM(Orcamento.valor)';
The paginate() stops working and display only one entry, like so:
Page 1 of 1, showing 1 records out of 2 total, starting on record 1, ending on 2
This is my index() function:
public function index($tipoOrcamento = null) {
$this->Orcamento->recursive = 0;
/*
$orcamentoSubprojetosTotal = $this->Orcamento->query(
"SELECT
SUM(Orcamento.valor) AS TotalOrcamentoSuprojetos
FROM
orcamentos AS Orcamento
WHERE
Orcamento.subprojeto_id IS NOT NULL;"
);
$this->set(compact('orcamentoSubprojetosTotal'));
*/
$this->set(compact('tipoOrcamento'));
if($tipoOrcamento == 'subtitulo'){
$this->set('orcamentos', $this->Paginator->paginate('Orcamento', array('Orcamento.subtitulo_id IS NOT NULL')));
}elseif($tipoOrcamento == 'subprojeto'){
$this->set('orcamentos', $this->Paginator->paginate('Orcamento', array('Orcamento.subprojeto_id IS NOT NULL')));
}else{
$this->set('orcamentos', $this->Paginator->paginate('Orcamento'));
}
}
Can I use the query() or someone can help me with the virtual field?
Thank you.
Do not use a virtual field
A virtual field is intended for things like:
public $virtualFields = array(
'name' => 'CONCAT(User.first_name, " ", User.last_name)'
);
If a virtual field is used from something which does not belong to the/a single row - it will not do what you're expecting as evident by the secondary effects on the find call paginate generates.
Use field
Instead, use the field method and pass in the expression:
$integer = $Model->field(
'SUM(valor)',
array('NOT' => array('subprojeto_id' => null))
);
Which will execute:
SELECT SUM(valor) from x where NOT (subprojecto_id IS NULL);
This will also return a scalar value, whereas calling query as shown in the question will return a nested array.
of course when you use SUM you'll get a single record
there are two things you can do:
create the virtualField just before the find() call and unset it just after the query.
using 'fields' => arra(...) in your paginator setting and list just the fields you need to retrieve and not the virtualField when you don't want to SUM
What is the best way of working with calculated fields of Propel objects?
Say I have an object "Customer" that has a corresponding table "customers" and each column corresponds to an attribute of my object. What I would like to do is: add a calculated attribute "Number of completed orders" to my object when using it on View A but not on Views B and C.
The calculated attribute is a COUNT() of "Order" objects linked to my "Customer" object via ID.
What I can do now is to first select all Customer objects, then iteratively count Orders for all of them, but I'd think doing it in a single query would improve performance. But I cannot properly "hydrate" my Propel object since it does not contain the definition of the calculated field(s).
How would you approach it?
There are several choices. First, is to create a view in your DB that will do the counts for you, similar to my answer here. I do this for a current Symfony project I work on where the read-only attributes for a given table are actually much, much wider than the table itself. This is my recommendation since grouping columns (max(), count(), etc) are read-only anyway.
The other options are to actually build this functionality into your model. You absolutely CAN do this hydration yourself, but it's a bit complicated. Here's the rough steps
Add the columns to your Table class as protected data members.
Write the appropriate getters and setters for these columns
Override the hydrate method and within, populate your new columns with the data from other queries. Make sure to call parent::hydrate() as the first line
However, this isn't much better than what you're talking about already. You'll still need N + 1 queries to retrieve a single record set. However, you can get creative in step #3 so that N is the number of calculated columns, not the number of rows returned.
Another option is to create a custom selection method on your TablePeer class.
Do steps 1 and 2 from above.
Write custom SQL that you will query manually via the Propel::getConnection() process.
Create the dataset manually by iterating over the result set, and handle custom hydration at this point as to not break hydration when use by the doSelect processes.
Here's an example of this approach
<?php
class TablePeer extends BaseTablePeer
{
public static function selectWithCalculatedColumns()
{
// Do our custom selection, still using propel's column data constants
$sql = "
SELECT " . implode( ', ', self::getFieldNames( BasePeer::TYPE_COLNAME ) ) . "
, count(" . JoinedTablePeer::ID . ") AS calc_col
FROM " . self::TABLE_NAME . "
LEFT JOIN " . JoinedTablePeer::TABLE_NAME . "
ON " . JoinedTablePeer::ID . " = " . self::FKEY_COLUMN
;
// Get the result set
$conn = Propel::getConnection();
$stmt = $conn->prepareStatement( $sql );
$rs = $stmt->executeQuery( array(), ResultSet::FETCHMODE_NUM );
// Create an empty rowset
$rowset = array();
// Iterate over the result set
while ( $rs->next() )
{
// Create each row individually
$row = new Table();
$startcol = $row->hydrate( $rs );
// Use our custom setter to populate the new column
$row->setCalcCol( $row->get( $startcol ) );
$rowset[] = $row;
}
return $rowset;
}
}
There may be other solutions to your problem, but they are beyond my knowledge. Best of luck!
I am doing this in a project now by overriding hydrate() and Peer::addSelectColumns() for accessing postgis fields:
// in peer
public static function locationAsEWKTColumnIndex()
{
return GeographyPeer::NUM_COLUMNS - GeographyPeer::NUM_LAZY_LOAD_COLUMNS;
}
public static function polygonAsEWKTColumnIndex()
{
return GeographyPeer::NUM_COLUMNS - GeographyPeer::NUM_LAZY_LOAD_COLUMNS + 1;
}
public static function addSelectColumns(Criteria $criteria)
{
parent::addSelectColumns($criteria);
$criteria->addAsColumn("locationAsEWKT", "AsEWKT(" . GeographyPeer::LOCATION . ")");
$criteria->addAsColumn("polygonAsEWKT", "AsEWKT(" . GeographyPeer::POLYGON . ")");
}
// in object
public function hydrate($row, $startcol = 0, $rehydrate = false)
{
$r = parent::hydrate($row, $startcol, $rehydrate);
if ($row[GeographyPeer::locationAsEWKTColumnIndex()]) // load GIS info from DB IFF the location field is populated. NOTE: These fields are either both NULL or both NOT NULL, so this IF is OK
{
$this->location_ = GeoPoint::PointFromEWKT($row[GeographyPeer::locationAsEWKTColumnIndex()]); // load gis data from extra select columns See GeographyPeer::addSelectColumns().
$this->polygon_ = GeoMultiPolygon::MultiPolygonFromEWKT($row[GeographyPeer::polygonAsEWKTColumnIndex()]); // load gis data from extra select columns See GeographyPeer::addSelectColumns().
}
return $r;
}
There's something goofy with AddAsColumn() but I can't remember at the moment, but this does work. You can read more about the AddAsColumn() issues.
Here's what I did to solve this without any additional queries:
Problem
Needed to add a custom COUNT field to a typical result set used with the Symfony Pager. However, as we know, Propel doesn't support this out the box. So the easy solution is to just do something like this in the template:
foreach ($pager->getResults() as $project):
echo $project->getName() . ' and ' . $project->getNumMembers()
endforeach;
Where getNumMembers() runs a separate COUNT query for each $project object. Of course, we know this is grossly inefficient because you can do the COUNT on the fly by adding it as a column to the original SELECT query, saving a query for each result displayed.
I had several different pages displaying this result set, all using different Criteria. So writing my own SQL query string with PDO directly would be way too much hassle as I'd have to get into the Criteria object and mess around trying to form a query string based on whatever was in it!
So, what I did in the end avoids all that, letting Propel's native code work with the Criteria and create the SQL as usual.
1 - First create the [get/set]NumMembers() equivalent accessor/mutator methods in the model object that gets returning by the doSelect(). Remember, the accessor doesn't do the COUNT query anymore, it just holds its value.
2 - Go into the peer class and override the parent doSelect() method and copy all code from it exactly as it is
3 - Remove this bit because getMixerPreSelectHook is a private method of the base peer (or copy it into your peer if you need it):
// symfony_behaviors behavior
foreach (sfMixer::getCallables(self::getMixerPreSelectHook(__FUNCTION__)) as $sf_hook)
{
call_user_func($sf_hook, 'BaseTsProjectPeer', $criteria, $con);
}
4 - Now add your custom COUNT field to the doSelect method in your peer class:
// copied into ProjectPeer - overrides BaseProjectPeer::doSelectJoinUser()
public static function doSelectJoinUser(Criteria $criteria, ...)
{
// copied from parent method, along with everything else
ProjectPeer::addSelectColumns($criteria);
$startcol = (ProjectPeer::NUM_COLUMNS - ProjectPeer::NUM_LAZY_LOAD_COLUMNS);
UserPeer::addSelectColumns($criteria);
// now add our custom COUNT column after all other columns have been added
// so as to not screw up Propel's position matching system when hydrating
// the Project and User objects.
$criteria->addSelectColumn('COUNT(' . ProjectMemberPeer::ID . ')');
// now add the GROUP BY clause to count members by project
$criteria->addGroupByColumn(self::ID);
// more parent code
...
// until we get to this bit inside the hydrating loop:
$obj1 = new $cls();
$obj1->hydrate($row);
// AND...hydrate our custom COUNT property (the last column)
$obj1->setNumMembers($row[count($row) - 1]);
// more code copied from parent
...
return $results;
}
That's it. Now you have the additional COUNT field added to your object without doing a separate query to get it as you spit out the results. The only drawback to this solution is that you've had to copy all the parent code because you need to add bits right in the middle of it. But in my situation, this seemed like a small compromise to save all those queries and not write my own SQL query string.
Add an attribute "orders_count" to a Customer, and then write something like this:
class Order {
...
public function save($conn = null) {
$customer = $this->getCustomer();
$customer->setOrdersCount($customer->getOrdersCount() + 1);
$custoner->save();
parent::save();
}
...
}
You can use not only the "save" method, but the idea stays the same. Unfortunately, Propel doesn't support any "magic" for such fields.
Propel actually builds an automatic function based on the name of the linked field. Let's say you have a schema like this:
customer:
id:
name:
...
order:
id:
customer_id: # links to customer table automagically
completed: { type: boolean, default false }
...
When you build your model, your Customer object will have a method getOrders() that will retrieve all orders associated with that customer. You can then simply use count($customer->getOrders()) to get the number of orders for that customer.
The downside is this will also fetch and hydrate those Order objects. On most RDBMS, the only performance difference between pulling the records or using COUNT() is the bandwidth used to return the results set. If that bandwidth would be significant for your application, you might want to create a method in the Customer object that builds the COUNT() query manually using Creole:
// in lib/model/Customer.php
class Customer extends BaseCustomer
{
public function CountOrders()
{
$connection = Propel::getConnection();
$query = "SELECT COUNT(*) AS count FROM %s WHERE customer_id='%s'";
$statement = $connection->prepareStatement(sprintf($query, CustomerPeer::TABLE_NAME, $this->getId());
$resultset = $statement->executeQuery();
$resultset->next();
return $resultset->getInt('count');
}
...
}