This has been bothering me for quite some time now.
Controller, on my take, is where I do validation, model calls, and displaying of data. And on the Model, which has only one purpose, is where I make my SQL queries.
But what is the best approach on Model. Do I have to make a lot of functions, with different conditions/approaches, or do I have to do it all in a single function, dedicated to a single table in my database. Take the code below for example:
Multiple Functions:
class Sword_model extends CI_Model {
public function __construct()
{
$this->load->database();
$this->load->library('session');
$this->load->helper('url_helper');
}
public function getsword($swordid = NULL)
{
if($swordid === NULL){
$query = $this->db->query("SELECT * FROM sword_tb");
return $query->result_array();
}
$query = $this->db->query("SELECT * FROM sword_tb WHERE sword_id = ?", array($swordid));
return $query->row_array();
}
public function getstrongswords($strong = NULL)
{
if($strong === NULL){
return false;
}
$query = $this->db->query("SELECT * FROM sword_tb WHERE strong = ?", array($strong));
return $query->result_array();
}
/*** AND MORE SUCCEEDING FUNCTIONS BELOW FOR DIFFERENT COLUMNS/CONDITIONS ***/
}
Pros: This is straight through and easier to understand (and I think is faster)
Cons: You have to manually create functions for different conditions/columns
Single Function:
class Sword_model extends CI_Model {
public function __construct()
{
$this->load->database();
$this->load->library('session');
$this->load->helper('url_helper');
}
public function getsword($column = NULL, $value = NULL, $condition = 'AND')
{
$query = 'SELECT * FROM sword_tb';
$count = count($column);
for($x = 0; $x < $count; $x++){
if($x > 0){
$query .= ' '.$condition;
} else {
$query .= ' WHERE (';
}
$query .= ' '.$column[$x].' = ?';
}
$query .= ' ORDER BY sword_name';
$query = $this->db->query($query, $value);
return $query->result_array();
}
}
with this single function approach, you can call this function by putting arrays as parameters like this:
$this->Sword_model->getsword(array('sword', 'strong'), array(3, 1), 'OR');
And the query will look like this:
SELECT * FROM sword WHERE (sword = ? OR strong = ?) ORDER BY sword_name
And if you left it behind blank, the query will look like this:
SELECT * FROM sword ORDER BY sword_name
Pros: Flexible
Cons: Slower (?) than the first approach
What is more ideal between the two? Or is there any other more ideal way?
I prefer to have each table to have it's own model with different preferable methods as you do in "Multiple Model". This way it is more maintainable and easier to approach & understand the codes. The second one is though it's harder to approach and it may not be useful on all scenario.
I have been using Codeigniter MVC & HMVC for more than 2 years. First one is my choice for so far and it helps me to check and maintain my codes and also helps in my future updates/codes.
You can try ORM ( Object-relational mapping )
Datamapper is an ORM library .It is designed to map your Database tables into easy to work with objects .
After successful installation of Datamapper
If you have a table called users
Create a model with name user
<?php
class User extends DataMapper {
function __construct($id = NULL)
{
parent::__construct($id);
}
}
In your controller, you can simply access data by
$u = new user(); // Singular of model name is required
$u->get();
foreach ($u as $user => $value)
{
echo $user->name;
}
By this method you have to create each models for your tables and access data through your controller
I prefer one generic model using which I can execute all mysql queries (which are not so complex, otherwise you need to write a query string and get it executed in few cases if there is a need of some too much complex query.) Otherwise you can call the models' generic functions from your controller by passing data array and table name and rest will be managed by the model.
Here is the generic model I use:
<?php
/*
All user module related databse functions
Author : Himanshu Upadhyay (himanshuvarun#gmail.com)
*/
if (!defined('BASEPATH'))
exit('No direct script access allowed');
class User_Model extends MY_Model {
public function __construct() {
parent::__construct();
}
public function get_rows($filters = array(), $table = TBL_USER) {
return parent::get_rows($filters, $table);
}
public function get_columns($table = TBL_USER) {
return parent::get_columns($table);
}
public function update_table($data, $where, $table = TBL_USER, $set = '') {
return parent::update_table($data, $where, $table, $set = '');
}
public function get_count($filters = array(), $table = TBL_USER) {
return parent::get_count($filters, $table);
}
public function insert($data, $table = TBL_USER) {
return parent::insert($data, $table);
}
public function delete($where, $table = TBL_USER) {
return parent::delete($where, $table);
}
/* End of file user_model.php */
/* Location: ./application/models/user_model.php */
?>
And in your controller, you can call the model function like :
$user_data['first_name'] = 'ABC';
$user_data['last_name'] = 'XYZ';
$this->user_model->insert($user_data, 'tbl_users'); // This is calling `insert` function of user model with first array argument with the data with column names as keys of the array and 2nd argument is the table name.
Conclusion: So by this approach, you can load user_model in all the controllers and you can use all its generic functions in all of the controllers. So this approach avoids redundant model functions to fetch, insert and update the data as well as it saves us by defining different models for each tables.
Most of the time multiple functions are easier to debug, comprehend and maintain.
That said, you could make your multi-method model code a lot less repetitive.
Consider the following. (__construct() not shown cause yours is fine.)
class Sword_model extends CI_Model
{
protected $get_all_sql = 'SELECT * FROM sword_tb';
protected $get_where_sql = "SELECT * FROM sword_tb WHERE sword_id = ?";
public function getsword($swordid = NULL)
{
$sql = isset($swordid) ? $this->get_where_sql : $this->get_all_sql;
$bind = isset($swordid) ? $swordid : FALSE;
return $this->do_Query($sql, $bind);
}
public function getstrongswords($strong = NULL)
{
if(isset($strong))
{
return $this->do_Query($this->get_where_sql, $strong);
}
//Returning an empty array means the controller can skip doing a conditional
//check for a model return === FALSE.
//foreach() won't choke on an empty array, but it will choke if passed FALSE
return array();
}
protected function do_Query($sql, $binds = FALSE)
{
$query = $this->db->query($sql, $binds);
return $query->result_array();
}
}
However, the "flexible" approach can be useful is certain circumstances.
The speed difference between "singles" vs "flexible" is negligible and not a consideration. What does need to be considered is that "flexible" quickly becomes unwieldy as you try to respond to more "conditions".
There is something that will make writing "flexible" model methods easier - easier to write, comprehend, debug and maintain. Instead of manually constructing query strings and passing them to $this->db->query() use Query Builder. It is designed exactly for situations where you need to conditionally build a query statement.
Your "flexible" version of getsword() has at least one limitation and that is that you cannot SELECT columns that are not part of the WHERE clause.
Using Query Builder here is one way you could implement a flexible method that builds queries for both AND WHERE and OR WHERE clauses. Hopefully the DocBlock before the method will provide some insight.
class Sword_model extends CI_Model
{
protected $get_all_sql = 'SELECT * FROM sword_tb';
protected $get_where_sql = "SELECT * FROM sword_tb WHERE sword_id = ?";
/**
*
* #param mixed $columns The columns to retrieve. Can be either a string
* e.g. 'title, content, date',
* or it can be an array e.g. array('title', 'content', 'date')
*
* #param array $where If provided, must be an associative array where
* the key => value is 'field_name' => value_to_match, e.g.
* array('title' => "Kill Bill")
* $where requires a different structure when the $condition argument is
* "OR". In this case the value part should provide multiple values.
* These values can be provided either in an array
* or a comma separated string list. For instance:
* As an array, $where = array('field_name' => array('val1', 'val2'));
* As a string, $where = array('field_name' => 'val1, val2'));
*
* #param string $condition For this example can be either 'AND' (default) or 'OR'
*/
public function getsword($columns = NULL, $where = NULL, $condition = 'AND')
{
if(!empty($columns)) //No $columns means SELECT *
{
$this->db->select($columns);
}
$condition = strtoupper($condition); //Don't assume
if(!empty($where))
{
if($condition === 'OR')
{
foreach($where as $key => $values)
{
if(is_string($values))
{
$values = explode(', ', $values);
}
if(is_array($values))
{
foreach($values as $matching)
{
$match = [$key => $matching];
$this->db->or_where($match);
}
}
else
{
$this->db->or_where($key, $values);
}
}
}
else
{
$this->db->where($where);
}
}
return $this->db
->order_by('sword_name')
->get("sword_tb")
->result_array();
}
}
You might look at the block
foreach($values as $matching)
{
$match = [$key => $matching];
$this->db->or_where($match);
}
and question the use of or_where() before calling where(). Query Builder is smart enough to know if any other WHEREs have been added and won't put "OR" in front of "WHERE" if it's not needed.
Let's look at some usage examples.
$this->sword_model->getsword();
Produces the query statement
SELECT * FROM sword_tb ORDER BY sword_name
$this->sword_model->getsword('model, sword_name, cost', array('model' => 'Broad'));
produces
SELECT model, sword_name, cost FROM sword_tb WHERE model = Broad ORDER BY sword_name
$this->sword_model->getsword(NULL, array('model' => 'Broad', 'cost <' => 100));
SELECT * FROM sword_tb WHERE model = Broad AND cost < 100 ORDER BY sword_name
$this->sword_model->getsword('model, sword_name, cost',
array('model' => 'Broad, Samurai', 'sword_name' => 'Excalibur'), 'or');
SELECT model, sword_name, cost FROM sword_tb WHERE model = Broad OR model = Samurai OR sword_name = Excalibur ORDER BY sword_name
$this->sword_model->getsword(NULL,
array('model' => 'Broad, Samurai', 'sword_name' => ['Excalibur', 'Fred']), 'or');
SELECT * FROM sword_tb WHERE model = Broad OR model = Samurai OR sword_name = Excalibur OR sword_name = Fred ORDER BY sword_name
NOTE: I removed the back-tics from the generated queries string because here on SO it looked weird. Rest assured, everything is properly escaped.
If you're worried about speed, I benchmarked the above. The longest time required to build a query statement was 0.0007 second. To build all five queries required a total of 0.0022 seconds - an average of 0.00044 each.
Note, this is time to build the statement. The time to retrieve data is not included because, well... I don't have the data.
Related
Let's say i'm making a web application for a online story sharing site.
We have tables: genre,tags,content_warning with only 2 columns as id and name. for example: genre = 1 - romance, 2 - action, etc.
I use the above tables to control/define each genre/tags/contentwarnings. so whenever a user publishes/uploads their own story they just cant input random tags/genre/contentwarnings.
So i'll have these functions in my story_model:
public function get_genre(){
$genre = array();
$query = $this->db->get('genre');
foreach($query->result() as $row){
$genre[$row->genre_id] = $row->genre_name;
}
return $genre;
}
public function get_tags(){
$tags = array();
$query = $this->db->get('tag');
foreach($query->result() as $row){
$tags[$row->tag_id] = $row->tag_name;
}
return $tags;
}
public function get_content_warnings(){
$content_warning = array();
$query = $this->db->get('content_warning');
foreach($query->result() as $row){
$content_warnings[$row->content_warning_id] = $row->content_warning_name;
}
return $content_warning;
}
Am i correct to say that i am repeating myself in the above 3 functions? so i would write a single code like:
public function sample_get($table){
$data = array();
$query = $this->db->get($table);
foreach($query->result_array() as $row){
$data[$row[$table.'_id']] = $row[$table.'_name'];
}
return $data;
}
to access the above function in my controller i would pass 'genre' or 'tag' or 'content_warning' as the parameter.
How would i name my above function then? the 3 separate functions are easy to name as they are very direct to the point and you know what the function does by reading its name.
combining all 3 function into one makes it harder to name the function in a direct way. I like to name my function as direct as possible so its easy for me to follow. as in they're more readable.
Account_model:
public function get_userid($username){
$this->db->select('user_id');
$this->db->from('user');
$this->db->where('username', $username);
$query = $this->db->get();
foreach($query->result() as $row){
$user_id = $row->user_id;
}
return $user_id;
}
story_model:
public function get_stories($user_id){
$stories = array();
$this->db->select('story_id, story_name');
$this->db->from('story');
$this->db->where('user_id', $user_id);//author
$query = $this->db->get();
foreach($query->result() as $row){
$stories[$row->story_id] = $row->story_name;
}
return $stories;
}
would the above two function warrant a dry-ification?
Or let's change the second function to only get the story_id which would coincide with the account_model function to get user_id. would they then be needed to dry?
but i get confused as to when do i decide to dry my functions? since i'll use alot of get function to retrieve data should i then just opt for a single get function?
If you want to use your common, abstract function, but keep well-named public functions, you can take the following approach:
/**
* Use the common function internally, but do not expose it publicly
*/
private function sample_get($table){
$data = array();
$query = $this->db->get($table);
foreach($query->result_array() as $row){
$data[$row[$table.'_id']] = $row[$table.'_name'];
}
return $data;
}
/**
* Use well-named functions that are exposed to be used publicly
*/
public function get_content_warnings(){
return $this->sample_get('content_warning');
}
This makes it possible to structure your class internally (through using the keyword private), but maintain a good interface to be used by other classes (through using public).
I am trying to make a base class ... tiny framework if you will just for practice
So I start with example of child class because it has less code !!
class User extends Base {
public $id ;
public $username ;
public $email ;
public $password ;
function __construct(){
$this->table_name = 'users';
$this->set_cols(get_class_vars('User'));
}
}
$u = new User;
$u->username = 'jason';
$u->email = 'j#gmail.com';
$u->insert();
Here is my Base class
class Base {
protected $table_name ;
protected $table_columns ;
protected function set_cols($cols){
unset($cols['table_name']);
unset($cols['table_columns']);
$this->table_columns = array_keys($cols);
}
public function insert(){
$colums = $values = array();
foreach($this->table_columns as $col )
{
if(!$this->$col) continue ;
$values[] = $this->$col ;
$colums[] = $col ;
}
$values = implode(',' , $values);
$colums = implode(',' , $colums);
echo $sql = "INSTER INTO ".$this->table_name ." ($colums)
VALUES ($values) ";
}
}
Here is the problem , I want to make filter or get method (basically reading from database) static and then return an array of objects from database data
class Base{
static function filter($conditions =array()){
$query_condition = $conditions ; // some function to convert array to sql string
$query_result = "SELECT * FROM ".$this->table_name ." WHERE $query_condition ";
$export = array();
$class = get_called_class();
foreach($query_result as $q )
{
$obj = new $class;
foreach($this->table_columns as $col )
$obj->$col = $q[$col];
$export[] = $obj;
}
return $export;
}
}
$users = User::filter(['username'=>'jason' , 'email'=>'j#gmail.com']);
Here is the problem , with filter as static function __construct in User class will not get called and table_columns, table_name will be empty
also in the filter method I can't access them anyway because they are not static ... I can make a dummy User object in the filter method and solve this problems but somehow it doesn't feel right
Basically I have a design problem any suggestion is welcomed
The problem is that the static object is not really "created" when you run statically.
If you want the constructor to run, but still in a static sort of way, you need a "singleton". This is where the object is created once and then you can re-use. You can mix this technique in a static and non-static way (as you're actually creating a "global" object that can be shared).
An example is
class Singleton {
private static $instance;
public static function getInstance() {
if (null === static::$instance) {
self::$instance = new static();
}
return self::$instance;
}
}
$obj = Singleton::getInstance();
Each time this gets the same instance and remembers state from before.
If you want to keep your code base with as few changes as possible, you can create yourself an "initialized" variable statically - you just need to remember to call it in each and every function. While it sounds great, it's even worse than a Singleton as it still remembers state AND you need to remember the init each time. You can, however, use this mixed with static and non-static calls.
class notASingletonHonest {
private static $initialized = false;
private static function initialize() {
if (!self::$initialized) {
self::$initialized = true;
// Run construction stuff...
}
}
public static function functionA() {
self::$initialize();
// Do stuff
}
public static function functionB() {
self::$initialize();
// Do other stuff
}
}
But read a bit before you settle on a structure. The first is far better than the second, but even then if you do use it, ensure that your singleton classes can genuinely be ran at any time without reliance on previous state.
Because both classes remember state, there are many code purists that warn you not to use singletons. You are essentially creating a global variable that can be manipulated without control from anywhere. (Disclaimer - I use singletons, I use a mixture of any techniques required for the job.)
Google "php Singleton" for a range of opinions and more examples or where/where not to use them.
I agree with a lot of your premises in your code and design. First - User should be a non static class. Second - Base base should have a static function that acts a factory for User objects.
Lets focus on this part of your code inside the filter method
1 $query_result = "SELECT * FROM ".$this->table_name ." WHERE $query_condition ";
2 $export = array();
3
4
5 $class = get_called_class();
6 foreach($query_result as $q )
7 {
8 $obj = new $class;
9
10 foreach($this->table_columns as $col )
11 $obj->$col = $q[$col];
12
13 $export[] = $obj;
14
15 }
The issue is that lines 1 and 10 are trying to use this and you want to know the best way to avoid it.
The first change I would make is to change protected $table_name; to const TABLE_NAME like in this comment in the php docs http://php.net/manual/en/language.oop5.constants.php#104260. If you need table_name to be a changeable variable, that is the sign of bad design. This will allow you change line 1 to:
$class = get_called_class()
$query_result = "SELECT * FROM ". $class::TABLE_NAME . "WHERE $query_condition";
To solve the problem in line 10 - I believe you have two good options.
Option 1 - Constructor:
You can rewrite your constructor to take a 2nd optional parameter that would be an array. Your constructor would then assign all the values of the array. You then rewrite your for loop (lines 6 to 15) to:
foreach($query_result as $q)
{
$export[] = new $class($q);
}
And change your constructor to:
function __construct($vals = array()){
$columns = get_class_vars('User');
$this->set_cols($columns);
foreach($columns as $col)
{
if (isset($vals[$col])) {
$this->$col = $vals[$col];
}
}
}
Option 2 - Magic __set
This would be similar to making each property public, but instead of direct access to the properties they would first run through a function you have control over.
This solution requires only adding a single function to your Base class and a small change to your current loop
public function __set($prop, $value)
{
if (property_exists($this, $prop)) {
$this->$prop = $value;
}
}
and then change line 10-11 above to:
foreach($q as $col => $val) {
$obj->$col = $val
}
Generally it is a good idea to seperate the logic of storing and retrieving the data and the structure of the data itself in two seperate classes. A 'Repository' and a 'Model'. This makes your code cleaner, and also fixes this issue.
Of course you can implement this structure in many ways, but something like this would be a great starting point:
class Repository{
private $modelClass;
public function __construct($modelClass)
{
$this->modelClass = $modelClass;
}
public function get($id)
{
// Retrieve entity by ID
$modelClass = $this->modelClass;
return new $$modelClass();
}
public function save(ModelInterface $model)
{
$data = $model->getData();
// Persist data to the database;
}
}
interface ModelInterface
{
public function getData();
}
class User implements ModelInterface;
{
public int $userId;
public string $userName;
public function getData()
{
return [
"userId" => $userId,
"userName" => $userName
];
}
}
$userRepository = new Repository('User');
$user = $userRepository->get(2);
echo $user->userName; // Prints out the username
Good luck!
I don't think there is anything inherently wrong with your approach. That said, this is the way I would do it:
final class User extends Base {
public $id ;
public $username ;
public $email ;
public $password ;
protected static $_table_name = 'users';
protected static $_table_columns;
public static function getTableColumns(){
if( !self::$_table_columns ){
//cache this on the first call
self::$_table_columns = self::_set_cols( get_class_vars('User') );
}
return self::$_table_columns;
}
public static function getTableName(){
return self::$_table_name;
}
protected static function _set_cols($cols){
unset($cols['_table_name']);
unset($cols['_table_columns']);
return array_keys($cols);
}
}
$u = new User;
$u->username = 'jason';
$u->email = 'j#gmail.com';
$u->insert();
And then the base class, we can use Late Static Binding here static instead of self.
abstract class Base {
abstract static function getTableName();
abstract static function getTableColumns();
public function insert(){
$colums = $values = array();
foreach( static::getTableColumns() as $col ){
if(!$this->$col) continue ;
$values[] = $this->$col ;
$colums[] = $col ;
}
$values = implode(',' , $values);
$colums = implode(',' , $colums);
echo $sql = "INSERT INTO ". static::getTableName() ." ($colums) VALUES ($values) ";
}
static function filter($conditions =array()){
$query_condition = $conditions ; // some function to convert array to sql string
$query_result = "SELECT * FROM ".static::getTableName() ." WHERE $query_condition ";
$export = array();
$columns = static::getTableColumns(); //no need to call this in the loop
$class = get_called_class();
foreach($query_result as $q ){
$obj = new $class;
foreach( $columns as $col ){
$obj->$col = $q[$col];
}
$export[] = $obj;
}
return $export;
}
}
Now on the surface this seems trivial but consider this:
class User extends Base {
public $id ;
public $username ;
public $email ;
public $password ;
final public static function getTableName(){
return 'users';
}
final public static function getTableColumns(){
return [
'id',
'username',
'email',
'password'
];
}
}
Here we have a completely different implementation of those methods from the first Users class. So what we have done is force implementation of these values in the child classes where it belongs.
Also, by using methods instead of properties we have a place to put custom logic for those values. This can be as simple as returning an array or getting the defined properties and filtering a few of them out. We can also access them outside of the class ( proper like ) if we need them for some other reason.
So overall you weren't that far off, you just needed to use static Late Static Binding, and methods instead of properties.
http://php.net/manual/en/language.oop5.late-static-bindings.php
-Notes-
you also spelled Insert wrong INSTER.
I also put _ in front of protected / private stuff, just something I like to do.
final is optional but you may want to use static instead of self if you intend to extend the child class further.
the filter method, needs some work yet as you have some array to string conversion there and what not.
Can I do 2 separate queries in a model in joomla 2.5 ?
If it is possible, how do you do this?
This is my model:
<?php
defined('_JEXEC') or die();
jimport( 'joomla.application.component.modellist' );
class AnagraficheModelServiziassociatiaggiuntivi extends JModelList
{
public function __construct($config = array())
{
if (empty($config['filter_fields']))
{
$config['filter_fields'] = array('id_servizi_aggiuntivi', 'id_persona', 'id_servizio', 'nome_servizio', 'cod_servizio', 'id_tipologia_socio', 'nome_servizio');
}
parent::__construct($config);
}
function getListQuery()
{
$db = JFactory::getDBO();
$query = $db->getQuery(true);
$query->select('id_servizi_aggiuntivi, id_persona , id_servizio , nome_servizio');
$query->from('#__servizi_aggiuntivi as serviziaggiuntivi, #__elenco_servizi');
$query->where('cod_servizio=id_servizio');
$result1 = $db->loadObjectList();
//$db->setQuery($query);
$query = $db->getQuery(true);
$query->select('id_tipologia_socio, id_servizio as cod_servizio, nome_servizio');
$query->from('#__associazione_servizi as serviziassociati, #__elenco_servizi');
$query->where('cod_servizio=id_servizio');
$result1 = $db->loadObjectList();
return $query;
}
protected function populateState($ordering = null, $direction = null)
{
// Load the filter state.
$search = $this->getUserStateFromRequest($this->context.'.filter.search', 'filter_search');
$this->setState('filter.search', $search);
// List state information.
parent::populateState($ordering, $direction);
}
}
?>
This doesn't work, but if I delete the second query it works great!
In my opinion, the problem is in the function getListQuery(). In this function I want insert 2 queries not 1 query! Is it possible?
The short answer is no. getListQuery should return a query. You can't get it to return two queries.
Instead, you likely want to override the getItems function to process the items after the first query runs. You can either adjust or add items using something like the following:
public function getItems() {
// load items from the parent getItems function, which will call getListQuery to handle your first query.
//It only adds items if the first query works.
if ($items = parent::getItems()) {
$db = JFactory::getDbo();
$query = $db->getQuery(true);
$query->select('id_tipologia_socio, id_servizio as cod_servizio, nome_servizio');
$query->from('#__associazione_servizi as serviziassociati, #__elenco_servizi');
$query->where('cod_servizio=id_servizio');
$result1 = $db->setQuery($query)->loadObjectList();
// this is a simple way to merge the objects. You could do other processing (loops, etc) to meld the information together
$items = (object) array_merge((array) $items, (array) $restult1);
}
return $items;
}
I have several doubts about OOP PHP code I'm creating. It is (so far) for retrieving a title and several chapters in different languages stored online. But first I will show the code as my doubts refer to this. This is the class I'm currently working with:
<?php
// Requires PHP 5.4+
class Subject
{
private $DB;
private $Language;
private $Keyword;
public function __construct($DB, $Keyword, $Language)
{
$this->DB=$DB;
$this->Keyword=$Keyword;
$this->Language=$Language;
}
private function query($query, $arg)
{
$STH = $this->DB->prepare($query);
$STH->execute(array_merge((array)$this->Keyword, (array)$arg));
return $STH->fetch()[$this->Language]; // PHP 5.4+
}
public function retrieveTitle ()
{
return $this->query("SELECT * FROM subject WHERE keyword = ? ORDER BY date DESC LIMIT 1");
}
public function retrieveChapter ($arg)
{
return $this->query("SELECT * FROM chapters WHERE subject_keyword = ? AND type = ? ORDER BY date DESC LIMIT 1", $arg);
}
?>
Then I do something similar to this to display the page:
if (isset($_GET['a']))
{
$Subject=new Subject($DB, $_GET['a'], $User->get('language'));
if ($Subject->retrieveTitle())
{
echo '<h1 id="Title">'.$Subject->retrieveTitle().'</h1>';
// Index
if ($Subject->retrieveTitle())
// ... code for the index
// Introduction
if ($Subject->retrieveChapter('Introduction'))
echo '<h2 id="Introduction">' . $_('Introduction') . '</h2>' . $Subject->retrieveChapter('Introduction');
// ... more non-relevant code.
}
}
else
// ... whatever
First concern. I'm not sure if this is the proper way to handle this kind of data. I tried to separate the methods and make them as small as possible, trying also not to repeat much code. And this is the way that feels right. But I cannot see why this other code, similar to the previous one, is less desirable. Note: This class has SURELY some typos and has not been tested, it's only here to ilustrate the difference, so please don't use it (at least not literaly):
<?php
// Requires PHP 5.4+
class Subject
{
public $Title;
public $Chapters = array ();
public function __construct($DB, $Keyword, $Language)
{
// Retrieve all
$STH = $DB->prepare("SELECT * FROM subject WHERE keyword = ? ORDER BY date DESC LIMIT 1");
$STH->execute(array($Keyword));
$this->Title = $STH->fetch()[$Language]; // PHP 5.4+
// Retrieve chapters
$ToForeach = ('Introduction','History','1');
$STH = $DB->prepare("SELECT * FROM chapters WHERE subject_keyword = ? AND type = ? ORDER BY date DESC LIMIT 1");
foreach ($ToForeach as $part)
{
$STH->execute(array($Keyword, $part));
$this->Chapters = $STH->fetch()[$Language];
}
}
}
?>
And then access the properties directly (or even build some get() in the middle, but you get the idea).
So, is there any difference? What are the benefits and pitfalls of the first method vs the second to code the class? Memory usage should be slightly smaller in the first one, but I think that won't be a deal breaker in comparison to readability in this case.
EDIT: Just writing the question in a way for others to understand has lead me to think about it in other ways. The first way also looks easier to test.
Second concern. If I want to make a/some methods for saving data, should I put it in the same class or in a different one? Because if I put it in one it bundles all the subject related methods in one pretty independent class, if I separate it I have more specialized classes with separated roles.
Any further advice, specifically about coding best practices [that I might not be following], is also welcome!
This is not easy to answer in a good way. So I can only focus on some minor parts of it. For not repeating code, I'd say the first example you give has quite some repeated code:
class Subject
{
/**
* #var ParametrizedQueryFetchQueryFactory
*/
private $queryFactory;
public function __construct($DB, $Keyword, $Language) {
$this->queryFactory = new ParametrizedQueryFetchQueryFactory($DB, $Language, [$Keyword]);
}
private function query($query, array $args = array()) {
return $this->queryFactory->query($query, $args);
}
public function retrieveTitle() {
return $this->query("SELECT * FROM subject WHERE keyword = ? ORDER BY DATE DESC LIMIT 1");
}
public function retrieveChapter($part) {
return $this->query(
"SELECT * FROM chapters WHERE subject_keyword = ? AND TYPE = ? ORDER BY DATE DESC LIMIT 1",
[$part]
);
}
}
class ParametrizedQueryFetchQueryFactory
{
private $db, $returnIndex, $defaultArgs;
public function __construct($db, $returnIndex, array $defaultArgs = array()) {
$this->db = $db;
$this->returnIndex = $returnIndex;
$this->defaultArgs = $defaultArgs;
}
public function query($query, array $args = array()) {
$fetcher = new ParametrizedQueryFetch($this->db,$query, $this->returnIndex, $this->defaultArgs);
return $fetcher->execute($args);
}
}
class ParametrizedQueryFetch
{
private $db, $query, $returnIndex, $defaultArgs;
public function __construct($db, $query, $returnIndex, array $defaultArgs = array()) {
$this->db = $db;
$this->query = $query;
$this->returnIndex = $returnIndex;
$this->defaultArgs = $defaultArgs;
}
public function execute(array $args) {
$args = array_merge($this->defaultArgs, $args);
$stmt = $this->db->prepare($this->query);
$stmt->excute($args);
return $stmt->fetch()[$this->returnIndex];
}
}
And btw, to make this PHP 5.3 compatible, you would only need to change a single line here.
I have a many to many relationships in my project (user_role, grades, user_role_grades). But I also have a requirement not to delete any data from my db. So, I have add a status column to the table, that connecting 2 tables to create many to many relationship. Now I want on
$userRole->getGrades()
get only those records, that in unite table (user_role_grades) has no status "0". For those, I`m trying to use doctrine sql filter.
namespace Bis\MpBundle\Filter;
use \Doctrine\ORM\Mapping\ClassMetaData;
class UserRoleGradeFilter extends \Doctrine\ORM\Query\Filter\SQLFilter
{
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if("Bis\DefaultBundle\Entity\UserRoleGrade" == $targetEntity->name){
return $targetTableAlias . '.status != 0';
}
return '';
}
}
So, it is called, for Bis\DefaultBundle\Entity\UserRole, but not for Bis\DefaultBundle\Entity\UserRoleGrade entity. Have anyone any ideas?
Or may be you have some other ideas, how I can do that?
I don't think this is possible, because it appends directly SQL.
Even if you'd try sth like SQL injection:
return $targetTableAlias . '.status != 0)) LEFT join the_other_table ON '
. $targetTableAlias . '.grades HAVING the_other_table.status = 0 ((';
It would probably collapse on statement like (())
You can call $targetEntity->getAssociationMappings()['yourFieldName']
and if joinTable key exist that mean your have manyToMany relation. Where yourFieldName your field with ManyToMany relation.
Correct table alias you can get from Doctrine\ORM\Query\SqlWalker::getSQLTableAlias().
I get SqlWalker with debug_backtrace, not an elegant solution but better I've not found.
/**
* Get SqlWalker with debug_backtrace
*
* #return null|SqlWalker
*/
protected function getSqlWalker()
{
$caller = debug_backtrace();
$caller = $caller[2];
if (isset($caller['object'])) {
return $caller['object'];
}
return null;
}
Full implementation of my addFilterConstraint method
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
{
if (empty($this->reader)) {
return '';
}
// The Doctrine filter is called for any query on any entity
// Check if the current entity is "pool aware" (marked with an annotation)
$poolAware = $this->reader->getClassAnnotation(
$targetEntity->getReflectionClass(),
PoolAware::class
);
if (!$poolAware) {
return '';
}
if (!$poolId = $this->getParameter('poolId')) {
return '';
}
$fieldName = $poolAware->getFieldName();
if (empty($fieldName)) {
return '';
}
if (!$sqlWalker = $this->getSqlWalker()) {
return '';
}
if (!isset($targetEntity->getAssociationMappings()[$fieldName])) {
return '';
}
$mapping = $targetEntity->getAssociationMappings()[$fieldName];
if (isset($mapping['joinColumns'])) {
// oneToMany relation detected
$table = $targetEntity->getTableName();
$columnName = $mapping['joinColumns'][0]['name'];
$dqlAlias = constant($targetEntity->getName() . '::MNEMO');
} elseif (isset($mapping['joinTable'])) {
// manyToMany relation detected
$dqlAlias = constant($mapping['targetEntity'] . '::MNEMO');
$component = $sqlWalker->getQueryComponent($dqlAlias);
// Only main entity in query is interesting for us,
// otherwise do not apply any filter
if ($component['parent']) {
return '';
}
$table = $mapping['joinTable']['name'];
$columnName = $mapping['joinTable']['inverseJoinColumns'][0]['name'];
} else {
return '';
}
$tableAlias = ($sqlWalker instanceof BasicEntityPersister)
? $targetTableAlias // $repository->findBy() has been called
: $sqlWalker->getSQLTableAlias($table, $dqlAlias);
$query = sprintf('%s.%s = %s', $tableAlias, $columnName, $this->getConnection()->quote(poolId));
return $query;
}
All our Doctrine models have MNEMO constant which is simple name of a model. Abc\Module\Model\Product has MNEMO product. This MNEMO is equivalent of _alias in Repository class. That's why we apply this value to $dqlAlias
Full code explanation you can read here