How can I refactor/ simplify the following php function? - php

I have a function that will return how many products there are on an invoice and it seems pretty straightforward to me but maybe it can use some refactoring?
public function getTotalProductsNumber(): int
{
$dataset = 'supplier_invoice_products inner join supplier_invoices as si using (supplier_invoice_id)';
$dbmSupplier = new Dbm_Supplier($dataset);
$whereAndOpt = $this->getConditionsAndOptions();
$where = $whereAndOpt['where'];
$opt = $whereAndOpt['opt'];
$select = 'sum(product_quantity) as sumTotal';
$invoiceTotalProductsNumber = $dbmSupplier->findFirstSimple($where, $select, $opt);
$invoiceTotalProductsNumber['sumTotal'] = (int)$invoiceTotalProductsNumber['sumTotal'];
return $invoiceTotalProductsNumber['sumTotal'];
}
How can I extract this into at least two functions?

For this kind of thing, ideally you'd use an ORM. As I'm not sure which you're using, if at all, I'd probably refactor it to something more in this direction so it feels closer to how an ORM would work:
public function getTotalProductsNumber(): int
{
$dataset = 'supplier_invoice_products inner join supplier_invoices as si using (supplier_invoice_id)';
$extras = $this->getConditionsAndOptions();
$queryBuilder = [
'dbm' => new Dbm_Supplier($dataset),
'where' => $extras['where'],
'opt' => $extras['opt']
];
return $this->sumOfProductQuantities($queryBuilder);
}
private function sumOfProductQuantities(array $queryBuilder): int
{
$select = 'sum(product_quantity) as sumTotal';
$row = $this->queryBuilderFirstRow($queryBuilder, $select);
return (int)$row['sumTotal'];
}
private function queryBuilderFirstRow(array $qb, string $select): array
{
return $qb['dbm']->findFirstSimple($qb['where'], $select, $qb['opt']);
}

I personally think this is pretty straightforward and nicely clean, but the only optimization I can suggest is to extract getConditionsAndOptions part as it seems to be used in some other several parts of your code. Look at this please:
public function getTotalProductsNumber(): int
{
$dataset = 'supplier_invoice_products inner join supplier_invoices as si using (supplier_invoice_id)';
$dbmSupplier = new Dbm_Supplier($dataset);
list($where, $opt) = $this->getWhereAndOpt();
$select = 'sum(product_quantity) as sumTotal';
$invoiceTotalProductsNumber = $dbmSupplier->findFirstSimple($where, $select, $opt);
$invoiceTotalProductsNumber['sumTotal'] = (int)$invoiceTotalProductsNumber['sumTotal'];
return $invoiceTotalProductsNumber['sumTotal'];
}
private function getWhereAndOpt(): array
{
$whereAndOpt = $this->getConditionsAndOptions();
return [
$whereAndOpt['where'],
$whereAndOpt['opt'],
];
}
Another point maybe name conventions. I can see you have used snail_case naming for Dbm_Supplier. It's better to be either dbm_supplier or DbmSupplier.

Related

PHP cascading (passing) subsequent returns through 3 functions

I have been through S/O and have not found this specific problem although my inexperience may mean I've searched by the wrong terms. Any help would be appreciated. Thank you.
Basically, I have three PHP functions (within a class named KeyPositions). The first is called by passing an argument; the second function is called by the first and then produces an array that is passed on to the third function. By using echo and print_r I can see that everything is in place as it is being passed. The problem is occurring on the return from the final function (the 3rd). While my foreach/echo test (just before the return) shows every element in the array is present, that is not what I'm getting once I look at what is returned to the original call. Here are the steps:
Originating call: $keypositions = KeyPositions::getKPSponsor($session_userid);
Second return (where the problem starts?): return self::getKPList($sponsor_list);
Third function: public static function getKPList($arr)
Test before return (shows array contents perfectly): foreach($kppositions as $s) { echo $s['kp_root'],"<br>"; }
Return statement: return $kppositions;
But when I go back to the original function call and run this test I only receive a subset of what I would've expected from the return (above): foreach($keypositions as $t) { echo $t['id'],"<br>"; }
Is there something that I'm missing when an array is passed from one function to the next? All arrays are associative. I intentionally did not include the lengthy code within each of the functions because the functions themselves seem to be performing perfectly based on the test I do just before each return (meaning what I expect to be inside the array is fully intact at each step). I believe this may have something to do with the passing of returns from one function to the next? Any direction here would be appreciated. Thank you.
Okay. Sorry. My thought was to not muddy things with all the code. Here it is. Keep in mind, this is for testing and not for final (e.g., no PHP injection work yet.)
public static function getKPSponsor($userid)
{
return self::getKPSponsors('positions.jobholder_uid = "' .$userid. '"');
}
public static function getKPSponsors($criteria = '')
{
$sponsor_levels = array();
$sponsor_list = array();
$criteria = " WHERE $criteria";
$db = Database::getInstance();
$mysqli = $db->getConnection();
$sql_query_sponsors = "SELECT positions.position_id AS id, jobholder_uid AS uid FROM positions $criteria";
$result_sponsors = $mysqli->query($sql_query_sponsors);
while ($sponsor_level = mysqli_fetch_assoc($result_sponsors)){
$sponsor_id = $sponsor_level['uid'];
$sponsor_level['items'] = self::getKPSponsors('positions.positionmgr_uid = "' .$sponsor_id. '"');
$sponsor_levels[] = $sponsor_level;
}
foreach($sponsor_levels as $r) {
$sponsor_list[] = $r['id'];
}
mysqli_free_result($result_sponsors);
unset($r);
return self::getKPList($sponsor_list);
}
public static function getKPList($arr)
{
$kppositions = array();
$db = Database::getInstance();
$mysqli = $db->getConnection();
$sql_query_keypositions = "SELECT key_positions.kp_id AS id, key_positions.kp_root, key_positions.kp_area, key_positions.c_ksa, key_positions.c_relationship, key_positions.c_rare, key_positions.urgency, positions.jobholder_uid, master.last_name, CONCAT(master.first_name,' ',master.last_name) AS full_name FROM key_positions INNER JOIN positions ON positions.position_id = key_positions.kp_sponsor INNER JOIN master ON positions.jobholder_uid = master.email_address WHERE key_positions.kp_sponsor IN (" . implode(",", $arr) . ");";
$result = $mysqli->query($sql_query_keypositions);
while ($kp3 = mysqli_fetch_assoc($result)) {
$kppositions[] = $kp3;
}
foreach($kppositions as $s) { // My testing ...
echo $s['kp_root'],"<br>";
}
mysqli_free_result($result);
return $kppositions;
}
In case this helps further, the output from my test BEFORE the return yields:
1000014
1000015
1000016
1000012
1000006
1000022
1000017
1000018
1000019
1000020
1000021
1000008
1000009
1000010
1000011
1000004
1000005
1000007
1000013
1000003
1000001
1000002
The output AFTER the return yields:
1000001
1000002

CakePHP3: How to change an association strategy on-the-fly?

I would like to change an association strategy (hasMany) on the fly to "in" (default) to "select". Because this will correct the result for this situation:
"Get all publishers and only the first five books":
$publishersTable = TableRegistry::getTableLocator()->get('Publishers');
$publishersTable->getAssociation('Books')->setStrategy('select');
$query = $publishersTable->find()
->contain(['Books'=> function(Query $q){
return $q->limit(5);
}]);
Unfortunately, Cake still using "in" to run the query and not "separated queries" and the result is only 5 publishers (and not all publishers with the first 5 books).
Is it possible to change the strategy on-the-fly?
Thanks in advance !
A hasMany association will always use a single separate query, never multiple separate queries. The difference between the select and subquery strategies is that one will directly compare against an array of primary keys, and the other against a joined subquery that will match the selected parent records.
What you are trying is to select the greatest-n-per-group, that's not possible with the built in association loaders, and it can be a little tricky depending on the DBMS that you are using, check for example How to limit contained associations per record/group? for an example for MySQL < 8.x using a custom association and loader.
For DBMS that do support it, look into window functions. Here's an example of a loader that uses native window functions, it should be possible to simply replace the one in the linked example with it, but keep in mind that it's not really tested or anything, I just had it laying around from some experiments:
namespace App\ORM\Association\Loader;
use Cake\Database\Expression\OrderByExpression;
use Cake\ORM\Association\Loader\SelectLoader;
class GroupLimitedSelectLoader extends SelectLoader
{
/**
* The group limit.
*
* #var int
*/
protected $limit;
/**
* The target table.
*
* #var \Cake\ORM\Table
*/
protected $target;
/**
* {#inheritdoc}
*/
public function __construct(array $options)
{
parent::__construct($options);
$this->limit = $options['limit'];
$this->target = $options['target'];
}
/**
* {#inheritdoc}
*/
protected function _defaultOptions()
{
return parent::_defaultOptions() + [
'limit' => $this->limit,
];
}
/**
* {#inheritdoc}
*/
protected function _buildQuery($options)
{
$key = $this->_linkField($options);
$keys = (array)$key;
$filter = $options['keys'];
$finder = $this->finder;
if (!isset($options['fields'])) {
$options['fields'] = [];
}
/* #var \Cake\ORM\Query $query */
$query = $finder();
if (isset($options['finder'])) {
list($finderName, $opts) = $this->_extractFinder($options['finder']);
$query = $query->find($finderName, $opts);
}
$rowNumberParts = ['ROW_NUMBER() OVER (PARTITION BY'];
for ($i = 0; $i < count($keys); $i ++) {
$rowNumberParts[] = $query->identifier($keys[$i]);
if ($i < count($keys) - 1) {
$rowNumberParts[] = ',';
}
}
$rowNumberParts[] = new OrderByExpression($options['sort']);
$rowNumberParts[] = ')';
$rowNumberField = $query
->newExpr()
->add($rowNumberParts)
->setConjunction('');
$rowNumberSubQuery = $this->target
->query()
->select(['__row_number' => $rowNumberField])
->where($options['conditions']);
$columns = $this->target->getSchema()->columns();
$rowNumberSubQuery->select(array_combine($columns, $columns));
$rowNumberSubQuery = $this->_addFilteringCondition($rowNumberSubQuery, $key, $filter);
$fetchQuery = $query
->select($options['fields'])
->from([$this->targetAlias => $rowNumberSubQuery])
->where([$this->targetAlias . '.__row_number <=' => $options['limit']])
->eagerLoaded(true)
->enableHydration($options['query']->isHydrationEnabled());
if (!empty($options['contain'])) {
$fetchQuery->contain($options['contain']);
}
if (!empty($options['queryBuilder'])) {
$fetchQuery = $options['queryBuilder']($fetchQuery);
}
$this->_assertFieldsPresent($fetchQuery, $keys);
return $fetchQuery;
}
}
Thanks #ndm but I found another shorter solution:
$publishersTable->find()
->formatResults(function ($results) use ($publishersTable) {
return $results->map(function ($row) use ($publishersTable) {
$row['books'] = $publishersTable->Books->find()
->where(['publisher_id'=>$row['id']])
->limit(5)
->toArray();
return $row;
});
});

Dynamic query ORM Laravel 4.2

Alright I have a GeneralModel written in CodeIgniter and a friend asked me if I could convert it into Laravel 4.2 for him. I was working on this and I think I have most of it correct but I am getting stuck at the select statement.
In CodeIgniter I have the following:
public function getData($table, $multiple = 1, $field = FALSE, $val = FALSE){
if($field != FALSE){
// WHERE in case of FIELD / VAL :)
$this->db->where($field, $val);
}
$query = $this->db->get($table);
if($multiple == 1){
// Multiple rows
return $query->result_array();
} else {
// One row
return $query->row_array();
}
}
Does anyone here knows how I can convert this function into Laravel 4.2 syntax?
I currently have:
public function getData($table, $multiple = 1, $field = FALSE, $val = FALSE){
$result = DB::table($table);
}
I got stuck pretty quickly since I have no idea how I can achieve the same in Laravel 4.2 with splitting up the sections of the query like I did with CodeIgniter.
You can chain methods in the same way:
public function getData($table, $multiple = 1, $field = FALSE, $val = FALSE)
{
$query = DB::table($table);
if ($field != FALSE) {
// WHERE in case of FIELD / VAL :)
$query->where($field, $val);
}
if ($multiple)
return $query->get();
else
return $query->first();
}
Laravel is similar in that you can use the Fluent Query Builder to build your queries in multiple stages prior to actually making the query. Once you know this, the translation is pretty straightforward:
public function getData($table, $multiple = 1, $field = FALSE, $val = FALSE)
{
$query = DB::table($table);
if($field){
// WHERE in case of FIELD / VAL :)
$query = $query->where($field, $val);
}
if($multiple) {
return $query->get();
}
return $query->first();
}
I don't think it's really good practice relying on Fluent though inside of an Eloquent model, but there are cases where that can't be helped. If the current objective is to convert the project to Laravel, there's probably calling code relying on the fact that this method exists. Converting the function to use Eloquent rather than Fluent will change the function's signature and cause other parts of the code to break, but it would look like this:
public function getData($multiple = true, $field = false, $val = false)
{
$query = $this;
if($field) {
$query = $query->where($field, $val);
}
if($multiple) {
return $query->get();
}
return $query->first();
}
The calling code itself can be modified to do the exact same function in Laravel like this:
// Instead of...
$result = $model->getData(1, 'field', 'value');
// You can do this:
$result = $model->where('field', 'value')->get();
// Or this if you'd rather not have multiple:
$result = $model->where('field', 'value')->first();
Using this function inside Eloquent (IMHO) in the long run doesn't really save you much, and instead is mostly just clutter.

Generating a php object, two levels deep

I'm new to php - objects and arrays, especially. Coming from a JavaScript world, I'm having a modicum of trouble understanding the right way to construct objects, that may easily be iterated.
I'd like to create an object (or array - although I suspect an object would be more suitable) with the following structure:
$client_body:
$cst:
$title: 'Unique string'
$copy: function_result()
$ser:
$title: 'Unique string'
$copy: function_result()
$imp
$title: 'Unique string'
$copy: function_result()
...
I've been trying with variations on the following, but with numerous errors:
$client_body = new stdClass();
$client_body->cst->title = 'Client case study';
$client_body->cst->copy = get_field('client_cst');
$client_body->ser->title = 'Our service';
$client_body->ser->copy = get_field('client_ser');
...
And it seems that, using this approach, I'd have to use a new stdClass invocation with each new top-level addition, which seems a little verbose.
Could someone point me in the right direction?
You can just typecast an array to an object:
$client_body = (object)array(
"cst" => (object)array(
"title" => "Unique string",
"copy" => function_result()
)
);
You can try this object class more OOP:
<?php
class ClientBody{
protected $cst;
protected $ser;
protected $imp;
public function __construct($cst = '', $ser ='', $imp = '')
{
$this->cst = $cst;
$this->ser = $ser;
$this->imp = $imp;
}
public function getCst()
{
return $this->cst;
}
public function getSer()
{
return $this->ser;
}
public function getImp()
{
return $this->imp;
}
public function setCst($value)
{
$this->cst = $value;
}
public function setSer($value)
{
$this->ser = $value;
}
public function setImp($value)
{
$this->imp = $value;
}
}
$myObject = new ClientBody('toto', 'titi', 'tata');
echo $myObject->getCst(); // output 'toto'
echo $myObject->getSer(); // output 'titi'
echo $myObject->getImp(); // output 'tata'
Or you could use json_decode($client_body, TRUE);

Using sum() sql with php ActiveRecord

I'm using ActiveRecord in my app. I need to add many objects by attribute. Currently I do it as follows:
foreach($objects as $object):
$result += $object->value;
endforeach;
But it is very slow. I think I could get the same result but more efficient by sql sum(), so de question is can ActiveRecord return me the sum() result?
The only aggregating function already implemented in the model is count(). For sum(), you'd have to use an SQL query. You can do that using different classes (Connection, Table or Model).
Here's using find_by_sql() of the Model (returns an array of models):
$result = FooModel::find_by_sql('SELECT SUM(`value`) AS sum FROM `foo_table`')[0]->sum;
Add custom model to your lib/Model.php file
public static function sum($args) {
$args = func_get_args();
$options = static::extract_and_validate_options($args);
$options['select'] = 'SUM('.$args[0]["sum"].')';
if (!empty($args) && !is_null($args[0]) && !empty($args[0]))
{
if (is_hash($args[0]))
$options['conditions'] = (isset($args[0]["conditions"]) ? $args[0]["conditions"] : array());
else
$options['conditions'] = call_user_func_array('static::pk_conditions',$args);
}
$table = static::table();
$sql = $table->options_to_sql($options);
$values = $sql->get_where_values();
return static::connection()->query_and_fetch_one($sql->to_s(),$values);
}
Then call it:
YourModel::sum(array('conditions' => 'userid = 3', 'sum' => 'your_column'));
or
YourModel::sum(array('sum' => 'your_column'));

Categories