SitePoint Sponsor

User Tag List

Results 1 to 17 of 17
  1. #1
    SitePoint Enthusiast
    Join Date
    Aug 2009
    Posts
    43
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)

    OOP deisgn question

    I'm pretty new to OOP and been programming in it successfully in terms of getting things to work but I have found I keep running into a similar problem and am not sure what to 'Google query' to get answers.

    Basically I have 3 classes, projects, clients, people. I run into the issue of what is the best way to access each from each other.

    For instance, I want to grab all projects based on the current client and there are a couple of ways I have thought of how to do this but do not know the 'best' way to do it.

    1: have a method in the client class that grabs all the project info, store a multi-dimensional array, foreach said array to extract data
    2: have a method in the client class that creates multiple project objects and store in an array to do a foreach on
    3: create a new object using the project class and pass the client id to it

    Advantages/Disadvantages (that I can think of)
    1: (probably the worst idea) creates possibility of multiple methods in multiple classes that do almost/same thing however leaves all the projects in the client object
    2: same as above but keeps everything more object oriented
    3: seems like the right choice but now there's no 'easy way' to connect the projects to the client (for instance if I have n number of clients i can no longer just do $client->projects->name (I know I could do ${'clients'.$x} and ${'projects'.x} but that seems un-needed, like I'm missing an important step/concept of oop).

    Any thoughts, opinions, insights from anyone? Any help is greatly appreciated.

    Thanks,
    Justin

  2. #2
    SitePoint Evangelist
    Join Date
    Aug 2005
    Location
    Winnipeg
    Posts
    498
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    http://docs.doctrine-project.org/pro...g-started.html

    While there are many disagree with the use of ORM (especially for simple projects) it will likely result in DRY'er code than you could realistically construct.

    Although the entities being modelled are not the same there are three with similar relationships.

    Cheers,
    Alex
    The only constant in software is change itself

  3. #3
    Foozle Reducer ServerStorm's Avatar
    Join Date
    Feb 2005
    Location
    Burlington, Canada
    Posts
    2,699
    Mentioned
    89 Post(s)
    Tagged
    2 Thread(s)
    Hi Justin,

    Are you using a Database or just Classes?

    Steve
    ictus==""

  4. #4
    SitePoint Enthusiast
    Join Date
    Aug 2009
    Posts
    43
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    @PCSpectra thanks, not sure that's what I was looking for though...

    @ServerStorm I am using a database, mysql, using prepared msyqli statements.

    At the moment I'm going with option 2 as it seems to make the most sense to me (keeping some sort of hierarchy while keeping things together and still all in object form). I'd still love to hear insight. An Object with an array of other objects in it seems a little weird but I am new to OOP.

  5. #5
    SitePoint Wizard bronze trophy Jeff Mott's Avatar
    Join Date
    Jul 2009
    Posts
    1,155
    Mentioned
    14 Post(s)
    Tagged
    0 Thread(s)
    You may be better off creating a 4th class -- let's call it ProjectDatabase. This class can be responsible for mapping objects to database tables, and it can have methods such as findProjectsByClient that would fetch the data and create the objects with the appropriate relations.

  6. #6
    SitePoint Enthusiast
    Join Date
    Aug 2009
    Posts
    43
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    hmm, yeah, that might be better..cause right now I just realized I created the horrible monstrosity of an object with an array of objects down to a 4th level...
    basically: $client->project[$x]->item[$y]->subitem[$z] :/

  7. #7
    Foozle Reducer ServerStorm's Avatar
    Join Date
    Feb 2005
    Location
    Burlington, Canada
    Posts
    2,699
    Mentioned
    89 Post(s)
    Tagged
    2 Thread(s)
    Quote Originally Posted by The14thGOD View Post
    hmm, yeah, that might be better..cause right now I just realized I created the horrible monstrosity of an object with an array of objects down to a 4th level...
    basically: $client->project[$x]->item[$y]->subitem[$z] :/
    Hi Justin,

    Anytime you get 'monstrosities' in objects, it is a good indicator for re-factoring. Are you willing to post your classes so we can help re-factor them? I am not sure but it 'sounds' like your objects are taking on roles that go beyond discrete logic although it is hard to tell given what you have said so far.

    Steve
    ictus==""

  8. #8
    SitePoint Enthusiast
    Join Date
    Aug 2009
    Posts
    43
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Sure, I'll just post a couple though, once I see how things should be done I should be able to convert the rest:

    Dependency Injector
    Code:
    <?php
    class Container{
    		public static $_db;
    		
    		public static function make_user(){
    			$user = new User();
    			$user->setDB(self::$_db);
    			return $user;
    		}
    		public static function make_client(){
    			$client = new Client();
    			$client->setDB(self::$_db);
    			return $client;
    		}
    		public static function make_project(){
    			$project = new Project();
    			$project->setDB(self::$_db);
    			return $project;
    		}
    		public static function make_revision(){
    			$revision = new Revision();
    			$revision->setDB(self::$_db);
    			return $revision;
    		}
    		public static function make_slide(){
    			$slide = new Slide();
    			$slide->setDB(self::$_db);
    			return $slide;
    		}
    	}
    ?>
    Project
    Code:
    <?php
    	/*
    	 * Project
    	 * 
    	 */
    	 class Project {
    	 	private $_db;
    	 	
    	 	public $id;
    	 	public $client_id;
    	 	public $name;
    	 	public $description;
    	 	public $modified;
    	 	public $cur_revision;
    	 	public $status;
    	 	
    	 	public $revisions = array();
    	 	public $users = array();
    	 	
    	 	public function __construct(){
    	 	
    	 	}
    	 	
    	 	public function set_project_info($id,$name,$modified,$current_revision,$client_id){
    	 		$this->id = $id;
    	 		$this->name = $name;
    	 		$this->modified = $modified;
    	 		$this->current_revision = $current_revision;
    	 		$this->cilent_id = $client_id;
    	 	}
    	 	//----------------------------------------------------------------------------------------
    		// Select Database Functions
    		//----------------------------------------------------------------------------------------
    		public function select_project($id){
    			$this->id = $id;
    			if($stmt = $this->_db->prepare("SELECT client_id,name,description,modified,current_revision FROM projects WHERE id = ? ")){
    				$stmt->bind_param('i',$this->id);	
    				$stmt->execute();
    				$stmt->bind_result($client_id,$name,$description,$modified,$current_revision);			
    				$stmt->fetch();		
    				
    				$this->client_id = $client_id;
    				$this->name = $name;
    				$this->description = $description;
    				$this->modified = $modified;
    				$this->current_revision = $current_revision;
    				
    				$stmt->close();
    			}
    		}
    		public function select_revisions(){
    			//Prep query
    	 		if($stmt = $this->_db->prepare("SELECT id,modified,revision_number FROM revisions WHERE project_id = ? AND client_id = ? ")){
    	 			$stmt->bind_param('ii',$this->id,$this->client_id);	 			
    				$stmt->execute();				
    				$stmt->bind_result($id,$modified,$revision_number);
    				while($stmt->fetch()){
    					$revision = Container::make_revision();
    					$revision->set_revision_info($id,$modified,$revision_number,$this->id,$this->client_id);
    					$this->revision[] = $revision;
    					unset($revision);
    				}
    				$stmt->close();
    			}
    		}
    		public function select_users(){
    			//Prep Query
    			if($stmt = $this->_db->prepare("SELECT u.id,u.name FROM users AS u RIGHT JOIN project_relationships AS p ON u.id = p.user_id WHERE p.id = ?")){
    				$stmt->bind_param('i',$this->id);
    				$stmt->execute();
    				$stmt->bind_result($user_id,$user_name);
    				while($stmt->fetch()){
    					$user = Container::make_user();
    					$user->set_user_info($user_id,$user_name);
    					$this->users[] = $user;
    					unset($user);
    				}
    				$stmt->close();
    			}
    		}
    		
    	 	//----------------------------------------------------------------------------------------
    		// Insert Database Functions
    		//----------------------------------------------------------------------------------------
    	 	public function add_project($client_id,$name,$description,$status){
    	 		$this->client_id = $client_id;
    	 		$this->name = $name;
    	 		$this->description = $description;
    	 		$this->modified = date('Y-m-d H:i:s');
    	 		$this->status = $status;
    	 		
    	 		//Prep query
    	 		if($stmt = $this->_db->prepare("INSERT INTO projects (client_id,name,description,modified,status) VALUES(?,?,?,?,?) ")){
    				$stmt->bind_param('issss',$this->client_id,$this->name,$this->description,$this->modified,$this->status);
    				$stmt->execute();
    				if($stmt->error){
    					return false;
    				}
    				$stmt->close();
    				return true;
    			}
    	 	}
    	 	
    	 	//----------------------------------------------------------------------------------------
    		// Connection Database Functions
    		//----------------------------------------------------------------------------------------
    		public function setDB($db){
    			$this->_db = $db;
    		}
    		public function close_db(){
    			$this->_db->close();
    		}
    	 }
    ?>
    There are probably several issues with the code...so any other things to point would also be greatly apperciated.
    I haven't gotten the hang of exceptions quite yet either...so the return false without closing the stmt are probably bad...

    Thanks for any help,
    Justin

  9. #9
    Foozle Reducer ServerStorm's Avatar
    Join Date
    Feb 2005
    Location
    Burlington, Canada
    Posts
    2,699
    Mentioned
    89 Post(s)
    Tagged
    2 Thread(s)
    Hi Justin,

    Your Project class is using an Active Record Pattern to manage your communication with the Db, which is is the simplest of the different ways in which databases are related to in an application. Active Record leads relatively high degree of coupling between the application code and database structure. However this coupling is likely far easier to manage than adopting a more complex database pattern. The next genesis of this could be using a Table Data Gateway Pattern, which resembles the Active Record, only you implement gateways to manipulate all rows in a specific table, I don't think it is required to do this; others may disagree.

    For testing I dropped in a database class that I use sometime for testing. You can see that the project class itself initiates the db so it has been removed for the container class.

    You can also see that I changed the visibility of your properties from public to protected. You don't want someone messing with these properties without using a method you enforce as a public api, so for this the only places most of these properties can be set is at the Project Class instantiation or through a setter. I have created one setter set_id(id) as originally you had this located as a parameter on the Project Class's select_project() method. However you reference $this->id in most of your other methods so being able to set an id and then directly call on of these methods provides greater flexibility.

    You have nice use of prepared statements and a clean coding/naming style, so it was good to work with your code.

    In my view not too much needing changing.
    Code:
    <?php
    class DB {
      static $dbh ;
      public function conn(){
        $db_type = 'mysql'; 
        $db_name = 'my_db';
        $user = 'my_user' ;
        $password = 'secret' ;
        $host = 'localhost' ;
        try {
           $dsn = "$db_type:host=$host;dbname=$db_name";
           $this->dbh = new PDO ( $dsn, $user, $password);
           $this->dbh->setAttribute(PDO::ATTR_PERSISTENT, true);
           $this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
        } catch ( PDOException $e ) {
           print "Error!: " . $e->getMessage () . "\n" ;
           die () ;
        }
         return $dbh;
      }
    }
    class Container{
      public static function make_project(){
          $project = new Project();
          return $project;
      }
    }
    /*
     * Project
     * 
     */
    class Project {
      private $_db;
      protected $id;
      protected $client_id;
      protected $name;
      protected $description;
      protected $modified;
      protected $cur_revision;
      protected $status;
      
      protected $revisions = array();
      protected $users = array();
      
      public function __construct(){
        $this->_db = DB::conn();
      }
      
      public function set_project_info($id,$name,$modified,$current_revision,$client_id){
          $this->id = $id;
          $this->name = $name;
          $this->modified = $modified;
          $this->current_revision = $current_revision;
          $this->cilent_id = $client_id;
      }
    
      pubic function set_id(id){
        $this->id = $id;
      }
      //----------------------------------------------------------------------------------------
      // Select Database Functions
      //----------------------------------------------------------------------------------------
      public function select_project(){
          if($stmt = $this->_db->prepare("SELECT client_id,name,description,modified,current_revision FROM projects WHERE id = ? ")){
              $stmt->bind_param('i',$this->id);    
              $stmt->execute();
              $stmt->bind_result($client_id,$name,$description,$modified,$current_revision);            
              $stmt->fetch();        
              
              $this->client_id = $client_id;
              $this->name = $name;
              $this->description = $description;
              $this->modified = $modified;
              $this->current_revision = $current_revision;
              
              $stmt->close();
          }
      }
      public function select_revisions(){
          //Prep query
          if($stmt = $this->_db->prepare("SELECT id,modified,revision_number FROM revisions WHERE project_id = ? AND client_id = ? ")){
              $stmt->bind_param('ii',$this->id,$this->client_id);                 
              $stmt->execute();                
              $stmt->bind_result($id,$modified,$revision_number);
              while($stmt->fetch()){
                  $revision = Container::make_revision();
                  $revision->set_revision_info($id,$modified,$revision_number,$this->id,$this->client_id);
                  $this->revision[] = $revision;
                  unset($revision);
              }
              $stmt->close();
          }
      }
      public function select_users(){
          //Prep Query
          if($stmt = $this->_db->prepare("SELECT u.id,u.name FROM users AS u RIGHT JOIN project_relationships AS p ON u.id = p.user_id WHERE p.id = ?")){
              $stmt->bind_param('i',$this->id);
              $stmt->execute();
              $stmt->bind_result($user_id,$user_name);
              while($stmt->fetch()){
                  $user = Container::make_user();
                  $user->set_user_info($user_id,$user_name);
                  $this->users[] = $user;
                  unset($user);
              }
              $stmt->close();
          }
      }
      
      //----------------------------------------------------------------------------------------
      // Insert Database Functions
      //----------------------------------------------------------------------------------------
      public function add_project($client_id,$name,$description,$status){
          $this->client_id = $client_id;
          $this->name = $name;
          $this->description = $description;
          $this->modified = date('Y-m-d H:i:s');
          $this->status = $status;
          
          //Prep query
          if($stmt = $this->_db->prepare("INSERT INTO projects (client_id,name,description,modified,status) VALUES(?,?,?,?,?) ")){
              $stmt->bind_param('issss',$this->client_id,$this->name,$this->description,$this->modified,$this->status);
              $stmt->execute();
              if($stmt->error){
                  return false;
              }
              $stmt->close();
              return true;
          }
      }
      
      //----------------------------------------------------------------------------------------
      // Connection Database Functions
      //----------------------------------------------------------------------------------------
      public function setDB($db){
          $this->_db = $db;
      }
      public function close_db(){
          $this->_db->close();
      }
    }
    Regards,
    Steve
    ictus==""

  10. #10
    SitePoint Enthusiast
    Join Date
    Aug 2009
    Posts
    43
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Thanks ServerStorm, I'll have to look into this when I have more time to dive into it. I'm sure I'll have a couple questions at some point based on a brief look at it.

  11. #11
    SitePoint Enthusiast
    Join Date
    Aug 2009
    Posts
    43
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    @ServerStorm - Finally had a chance to look at the code and I only have one question about the construct of the Project class calling a new connection. If I had say 4-6 projects and they are all creating a new db connection inst that bad or is it effectively the same thing as what I was doing? I was just reusing the same db connection (not sure if that's bad as well...) and passing it through to the objects.

    Also, would you recommend the __set/__get magic methods or make a set_PROPERTY method for each property?

  12. #12
    Foozle Reducer ServerStorm's Avatar
    Join Date
    Feb 2005
    Location
    Burlington, Canada
    Posts
    2,699
    Mentioned
    89 Post(s)
    Tagged
    2 Thread(s)
    You want to share the database handle in an application because it's an overhead to keep opening and closing connections, particularly during a single page fetch.
    Quote Originally Posted by The14thGOD View Post
    @ServerStorm - Finally had a chance to look at the code and I only have one question about the construct of the Project class calling a new connection. If I had say 4-6 projects and they are all creating a new db connection inst that bad or is it effectively the same thing as what I was doing? I was just reusing the same db connection (not sure if that's bad as well...) and passing it through to the objects.

    Also, would you recommend the __set/__get magic methods or make a set_PROPERTY method for each property?
    Hi The14thGOD,
    You might want to share things like the database handle in your application because of the overhead to keep opening and closing connections, particularly when doing a single page fetch. Is it bad not to not share the db handle? It depends on how many connections you establish; it may be fine doing it your original way in the container or via the constructor.

    I put this in the constructor of the Projects class so that Project class knows its' own dependencies. It again is debatable whether you do it your way or the constructor way as neither are perfect, but can certainly be valid choices.

    Here is an example of how you might use a Singleton to share your Db connections:
    PHP Code:
    class ConnectionFactory{
        private static 
    $factory;
        private 
    $db;
        public static function 
    getFactory(){
            if (!
    self::$factory){
                
    self::$factory = new ConnectionFactory(...);
                return 
    self::$factory;
         }
       }
       public function 
    getConnection(){
            if (!
    $db){
                
    $db = new PDO(...);
                return 
    $db
           }
      }
    }

    // Usage
    $o_Db ConnectionFactory::getFactory()->getConnection(); 
    This can get a little tricky if you need to do connection pooling in the future. if you do need to do this then it would be implemented in the getConnection() method.

    As far as the __set/__get magic, it can make it hard for others and even yourself (long times away from the code to see what is happening; although your application would be good to do this as your set/get requirements shown are straight forward so hard to get lost in. Of course this can change

    Regards,
    Steve
    ictus==""

  13. #13
    SitePoint Enthusiast
    Join Date
    Aug 2009
    Posts
    43
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Thanks Steve, this info has been really helpful. I switched over to the method you first provided (db class) and just dropped the container class completely.

    i also just realized we strayed away from the original question of this post haha.

    Which (narrowed down) was basically:
    PHP Code:
    public function select_revisions(){
        
    //Prep query
         
    if($stmt $this->_db->prepare("SELECT id,modified,revision_number FROM revisions WHERE project_id = ? ORDER BY revision_number DESC")){
             
    $stmt->bind_param('i',$this->id);                 
            
    $stmt->execute();                
            
    $stmt->bind_result($id,$modified,$revision_number);
            while(
    $stmt->fetch()){
                
    $revision = new Revision();
                
    $revision->set_revision_info($id,$modified,$revision_number,$this->id,$this->client_id);
                
    $this->revisions[] = $revision;
                unset(
    $revision);
            }
            
    $stmt->close();
        }

    or something similar to:
    PHP Code:
    public function select_revisions(){
    //get all revisions based on project id and return them as an array
        
    $revisions Revision::select_revisions($this->id);

    Which would then call a function in the revision class that is basically the same as the first code section above.

    Is this the best way to approach this or do you have any other insights I should pursue? The reason why I'm worried is because the relationship is like this:
    Client->project->revisions->images->comments

    In a variable way it would look something like:
    $client->project[0]->revision[0]->images[0]->comments[0]

    This would access the first comment of the first image of the first revision on project 1 for client X.

    This is the only way I can think of to try and keep all the different classes separate but still be able to access them and keep them related to each project.
    If this doesn't make sense I can try and explain it better...

    thanks again for all of your help,
    Justin

  14. #14
    Foozle Reducer ServerStorm's Avatar
    Join Date
    Feb 2005
    Location
    Burlington, Canada
    Posts
    2,699
    Mentioned
    89 Post(s)
    Tagged
    2 Thread(s)
    Hi Justin,

    So that we can better understand, can you in words describe how clients, projects, revision, images and comments. This exercise might help us decide if the class modeling you have done is appropriate or if we refactor it in a different way.

    Regards,
    Steve
    ictus==""

  15. #15
    SitePoint Enthusiast
    Join Date
    Aug 2009
    Posts
    43
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Each client has multiple projects, each of these projects have revisions (versions of a project basically). Each revision has a series of images (as an example lets say that project is a website. Images could include: home, contact, about, etc). Each image has comments (users (who are attached to the client) can make comments about how that page is designed etc).

    Hopefully that clears it up a bit... I know it can be hard to understand esp without images/wireframes.

    Thanks again,
    Justin

  16. #16
    SitePoint Wizard bronze trophy
    Join Date
    Jul 2006
    Location
    Augusta, Georgia, United States
    Posts
    4,052
    Mentioned
    16 Post(s)
    Tagged
    3 Thread(s)
    What your talking about is really complicated topic and much the reason for libraries such as; doctrine exist. If you want to look more into it I suggest reading about lazy and eager loading. That is essentially what you are trying to achieve which are very advanced topics that tend to both have their strong and low points. Wish it were more simple but really it is not. You either run multiple queries to gather the data (lazy loading) or use joins and map that data to the proper model instance (eager loading). Which can become very painful if you don't have an automated/generic library to do it with. I'll expand on it some more based on your example.

    $client->project[0]->revision[0]->images[0]->comments[0]

    In the "lazy" methodology

    $client->project

    Would result in a hit to the db for all projects for the client.

    $client->project[0]->revision

    Would result in yet another query for the projects revisions

    $client->project[0]->revision[0->images

    Would result in yet another query for all the revisions images.

    $client->project[0]->revision[0]->images[0]->comments

    Would result in yet another query for all the images comments.

    Now this may not look to bad if you only dealing with a single project on the page. However, imagine if you had to list 50 projects on the page and the active revisions image. That could potentially be over 200 queries. This is a very real problem and one even the creator pf the phpdatamapper had never concurred in automated fashion besides for eager loading and custom mapping

    The other option is taking the hit initially, storing the objects in a cache and fetching them from there before hitting the db. That is the approach libraries such doctrine take ( I think phpdatamapper uses caching to). However, that means that you will always have to fetch all data associated with the object. What are referred to as "partials" only a specific collection of fields from a table/object is not really recommended given a caching model since objects can easily become out of sync that way. Especially a caching model that persists beyond the lifetime of a page request using something like memcache, which I believe is the preferred method in Doctrine.

    Now the other option – eager loading.

    Eager loading would be to create a big, gigantic query that performs all the joins necessary to collect all that data. This would ideally be a single query. Than map that data to each domain level instance, which can be very tricky. In this case it would because there are so many levels of the hierarchy. Not to mention you would really need to make this a single method because it wouldn't be practical to fetch all this other, related data if you only needed something at the root, project level. Though the obvious advantage of the eager method is that given 50 items a single query could be executed to collect the relational data for each level of the relational hierarchy.

    Not having a way to automate either eager or lazy loading is going to result in some painful, redundant code. That is much the reason libraries like doctrine are popular. To deal with mess of converting/mapping database tables or collection of tables to domain level entities that can be managed in a object oriented fashion. I'm not saying use doctrine but yeah… it is a complex topic.

    If your not using a library to mange the mapper implementation than you are probably better off using a dao and returning associative arrays. That would be nothing more than creating a class to manage "projects" and methods that return exactly the data set you need given a circumstance. That is actually the model I am using on a in progress CMS I'm working on for many of the reasons mentioned above. Here is a quick example just so you can get an idea.

    PHP Code:
    <?php 
    $this
    ->import('App.Core.DAO');
    /*
    * Site data access layer 
    */
    class MCPDAOSite extends MCPDAO {
        
        
    /*
        * List all sites
        * 
        * @param str select fields
        * @param str where clause
        * @param order by clause
        * @param limit clause
        * @return array users
        * 
        * @todo convert to variable binding - support it
        */
        
    public function listAll($strSelect='s.*',$strFilter=null,$strSort=null,$strLimit=null) {
            
            
    /*
            * Build SQL 
            */
            
    $strSQL sprintf(
                
    'SELECT
                      %s %s
                   FROM
                      MCP_SITES s
                      %s
                      %s
                      %s'
                
    ,$strLimit === null?'':'SQL_CALC_FOUND_ROWS'
                
    ,$strSelect
                
    ,$strFilter === null?'':"WHERE $strFilter"
                
    ,$strSort === null?'':"ORDER BY $strSort"
                
    ,$strLimit === null?'':"LIMIT $strLimit"
            
    );
            
            
    $arrSites $this->_objMCP->query($strSQL);
            
            
    /*
            * Load extra XML data 
            */
            
    foreach($arrSites as &$arrSite) {
                
    $arrSite $this->_loadXMLSiteData($arrSite);
            }
            
            if(
    $strLimit === null) {
                return 
    $arrSites;
            }
            
            return array(
                
    $arrSites
                
    ,array_pop(array_pop($this->_objMCP->query('SELECT FOUND_ROWS()')))
            );
            
        }
        
        
    /*
        * Fetch site data by sites id 
        * 
        * @param int site id
        * @return array site data
        */
        
    public function fetchById($intId) {
            
            
    /*$strSQL = sprintf(
                'SELECT %s FROM MCP_SITES WHERE sites_id = %s'
                ,$strSelect
                ,$this->_objMCP->escapeString($intId)
            );*/
                
            
    $arrSite array_pop($this->_objMCP->query(
                
    'SELECT * FROM MCP_SITES WHERE sites_id = :sites_id'
                
    ,array(
                    
    ':sites_id'=>(int) $intId
                
    )
            ));
            
            if(
    $arrSite !== null) {
                
    $arrSite $this->_loadXMLSiteData($arrSite);
            }
            
            return 
    $arrSite;
            
        }
        
        
    /*
        * Update/insert data data - logic includes saving XML stored fields properly
        * 
        * @param array site data
        * @return int affected rows/sites id
        */
        
    public function save($arrSite) {
            
            
    /*
            * Get fields native to sites table
            */
            
    $schema $this->_objMCP->query('DESCRIBE MCP_SITES');
            
            
    $native = array();
            foreach(
    $schema as $column) {
                
    $native[] = $column['Field'];
            }
            
            
    /*
            * Siphon dynamic fields
            */
            
    $dynamic = array();
            
            foreach(
    array_keys($arrSite) as $field) {
                if(!
    in_array($field,$native)) {
                    
    $dynamic[$field] = $arrSite[$field];
                    unset(
    $arrSite[$field]);
                }
            }
            
            
    /*
            * When creating a new site generate a random salt
            * This is a one time only thing otherwise data corruption of encrypted values would occur.
            * The SALT should never be exposed to the front-end API.
            * 
            * NEVER overwrite the salt. The salt is used for one way encrypting users passwords. If
            * the salt is lost ALL passwords MUST be reset. other things may also rely on the salt
            * but the most obvious one is user passwords. A sites salt should always be used when data
            * encrytion is needed.
            */
            
    if(!isset($arrSite['sites_id'])) {
                
    $dynamic['site_salt'] = sha1(time().time().'nautica');
            }
            
            
    /*
            * Update/insert the site data stored in the db
            */
            
    $intId $this->_save(
                
    $arrSite
                
    ,'MCP_SITES'
                
    ,'sites_id'
                
    ,array('site_name','site_directory','site_module_prefix')
                ,
    'created_on_timestamp'
            
    );
            
            
    /*
            * When updating existing site the return value of save is number of rows affected 
            */
            
    $intId = isset($arrSite['sites_id'])?$arrSite['sites_id']:$intId;
            
            if( !
    $intId ) {
                
    // @todo: error or throw exception
                
    return;
            }
            
            
    /*
            * When a new site is successfully created make site folder
            */
            
    if(!isset($arrSite['sites_id']) && $intId) {
                
                
    $dir ROOT.DS.'Site'.DS.$arrSite['site_directory'];
                
                
    /*
                * Attempt to create directory, if directory can't be created
                * an error either needs to be thrown or a message may need
                * to displayed telling the user to create the directory
                * manually.
                */
                
    if(!mkdir($dir)) {
                    
    // @todo error or throw exception
                    
    return;
                }
            }
            
            
    /*
            * Save site data stored inside XML config file 
            */
            
    $this->_saveXMLSiteData($dynamic,$intId);
            
            
    //echo '<pre>',print_r($arrSite),'</pre>';
            //echo '<pre>',print_r($dynamic),'</pre>';
            
        
    }
        
        
    /*
        * Load data for site not stored in database, such as domain, salt, etc
        * stored inside Main config file stored above site root.
        * 
        * @param array site
        * @return array site w/ mixin data
        */
        
    private function _loadXMLSiteData($site) {
            
            
    /*
            * Load XML config file 
            */
            
    $objXML simplexml_load_file(CONFIG.'/Main.xml');
            
            
    /*
            * Get the site node 
            */
            
    $node array_pop($objXML->xpath("//site[@id='{$site['sites_id']}']"));
            
            if(
    $node === null) return $site;
            
            
    /*
            * Recursive function to map XML to flat site array keys
            */
            
    $func = function($node,$path,$func) use (&$site) {
                
                if(!
    $node->children()) {
                    
    $site[$path] = (string) $node;
                    return;
                }
                
                foreach(
    $node->children() as $child) {
                    
    call_user_func($func,$child,"{$path}_{$child->getName()}",$func);
                }
            };
            
            
    /*
            * Map XML data 
            */
            
    call_user_func($func,$node,'site',$func);
            
            return 
    $site;
            
        }
        
        
    /*
        * Save data stored inside XML configuration file
        * 
        * @param array site XML fields
        * @param int sites id
        */
        
    private function _saveXMLSiteData($arrData,$intSitesId) {
            
            
    // echo '<pre>',print_r($arrData),'</pre>';
            
            /*
            * The necessary modifications require DOMDocument over simplexml
            */
            
    $objXML = new DOMDocument();
            
            
    /*
            * Load the XML site config file 
            */
            
    if( $objXML->loadCONFIG.'/Main.xml' ) === false) {
                
    // @todo: error or throw exception when file can't be loaded
                
    return;
            }
            
            
    /*
            * Use xpath to determine whether a definition exists for the site
            * or not. When a definition exists for the site use that node
            * otherwise start a new.
            */
            
    $objXPath = new DOMXPath$objXML );
            
            
    /*
            * Attempt to locate a node for the sites configuration definition.
            */
            
    $objResult $objXPath->query("//site[@id='$intSitesId']");
            
            
    /*
            * Check to make sure the query was at least well formed, otherwise go no further. A 
            * well-formed query without a result will return an empty result set. On the otherhand,
            * a query that is malformed will return false. 
            */
            
    if( $objResult === false ) {
                
    // @todo: error or throw some type of exception
                
    return;
            }
            
            
    // Get the matched node, this will be null if nothing was matched IE. definition for site doesn't exist
            
    $objSite $objResult->item(0);
            
            
    /*
            * When a site doesn't exist configure a new site element node entry. Otherwise,
            * use the node that was matched and overwrite as necessary.
            */
            
    if( $objSite === null) {
                
                
    /*
                * In this case create a new node 
                */
                
    $objSite = new DOMElement('site');
                
                
    /*
                * Add the new node to the document and refresh/reset reference 
                */
                
    $objSite $objXML->documentElement->appendChild($objSite);
                
                
    /*
                * Add the id attribute 
                */
                
    if( $objSite->setAttribute('id',(string) $intSitesId) === false) {
                    
    // @todo: error or throw exception
                    
    return;
                }
                
                
    /*
                * Mixin database authentication info based the default (site 0). For security
                * reasons this information is not controllable neither viewable from the front-end
                * interface. It is set one here when creating new sites and may be modified
                * manually, if need be.
                */
                
    $arrData['site_db_pass'] = $objXPath->query("//site[@id='0']/db/pass")->item(0)->nodeValue;
                
    $arrData['site_db_user'] = $objXPath->query("//site[@id='0']/db/user")->item(0)->nodeValue;
                
    $arrData['site_db_host'] = $objXPath->query("//site[@id='0']/db/host")->item(0)->nodeValue;
                
    $arrData['site_db_db'] = $objXPath->query("//site[@id='0']/db/db")->item(0)->nodeValue;
                
            }
            
            
    /* -------------------------------------------------------------------------------------- */
            // Begin modifying/adding the actual data on the site node as neccessary
            
            /*
            * Array defines the available site entry child nodes 
            */
            
    $arrStructure = array(
                 
    'domain'=>true
                
    ,'db'=>array(
                    
    'pass'=>true // perhaps good to use a default here? - for new entries
                    
    ,'user'=>true // perhaps good to use a default here? - for new entries
                    
    ,'host'=>true // perhaps good to use a default here? - for new entries
                    
    ,'db'=>true // perhaps good to use a default here? - for new entries
                    
    ,'adapter'=>true
                
    )
                ,
    'salt'=>true
            
    );
            
            
    /*
            * Recursive function used to update and add necessary nodes and values
            * to the site node. This accounts for all cases including partial and full
            * updates or full create.
            */
            
    $func = function($objNode,$arrStructure,$strAncestory,$func) use (&$arrData) {
                
                foreach(
    $arrStructure as $strName => $mixValue) {
                    
                    
    // Get the corresponding node
                    
    $objChild $objNode->getElementsByTagName($strName)->item(0);
                    
                    if( 
    is_array($mixValue) ) {
                        
                        
    // It need to be created at this point
                        
    if( $objChild === null ) {
                            
    $objChild $objNode->appendChild( new DOMElement($strName) );
                        }
                        
                        
    $func($objChild,$mixValue,"$strAncestory{$strName}_",$func);
                        
                    } else {
                        
                        
    // check that a value exists within the data array. If not move on.
                        
    if( !isset( $arrData["$strAncestory$strName"] ) ) {
                            continue;
                        }
                        
                        
    // if the node doesn't exist create it
                        
    if( $objChild === null ) {
                            
    $objChild $objNode->appendChild( new DOMElement($strName) );
                        }
                        
                        
    // Set the value
                        
    $objChild->nodeValue = (string) $arrData["$strAncestory$strName"];
                        
                    }
                    
                }
                
            };
            
            
    $func($objSite,$arrStructure,'site_',$func);
            
            
    /*
            * Before save nicely format output 
            * 
            * Reload the XML to format it properly. There seems
            * to be an issue with formating the XML before
            * reloading it. None the less, this takes care of the issue
            * and properly formats the XML.
            */
            
    $strXML $objXML->saveXML();
            
    $objXML = new DOMDocument();
            
    $objXML->preserveWhiteSpace false;
            
    $objXML->formatOutput true;
            
    $objXML->loadXML($strXML);
            
            
    /*
            * Save XML back to file (test file for now)
            */
            
    if( $objXML->saveCONFIG.'/Main.xml' ) === false) {
                
    // @todo: something went wrong - XML was not saved - throw exception or raise error
                // echo "<p>Could not write xml file</p>";
                // exit;
                
    return;
            }
            
            return;
            
            
    //return;
            /* -----------------------------------------------------------------------------------------------
            * TEST result: Seems to be functioning - test with new site
            * Seems to be working properly with new site and update - need to add
            * default info for database that is a copy of the site 0
            */
            
    ob_clean();
            
    header('Content-Type: text/xml');
            echo 
    $objXML->saveXML();
            exit;
            
            
        }
        
    }
    ?>
    The main idea is that I forget about mapping and merely encapsulate all data access logic in this class – simple. Not nearly as powerful or cool as active record or data mapper but it is much less code and I don't mind writting queries. So yeah…

    The only thing that tends to suck is listAll methods which I need to control filters, sorting, limits, etc from outside. The way it is now my application code does need to know something about the tables which are being hit, but I have yet to figure out an elegant way to change that without a whole bunch more code or inventing my own ActiveRecord or ORM.
    The only code I hate more than my own is everyone else's.

  17. #17
    SitePoint Enthusiast
    Join Date
    Aug 2009
    Posts
    43
    Mentioned
    0 Post(s)
    Tagged
    0 Thread(s)
    Oddz, thanks for the detailed post. I will look more into the 'lazy' and 'eager' loading methods, I think I got a pretty good concept, but more details are always good to have. Not sure if it applies to all the tools, but I know I can't use memcache because I'm on a shared host. If I had the money I'd shell out for a dedicated server. When you mention 'map to domain' does that refer to matching say /project/ with the project table? Similar to how the MVC method works? (which I still don't get, I understand the concept, but actually putting it into practice I get lost (topic for another day)). I'll have to look at more info on the rest of your post as it's over my head a bit but I am glad to know it's not an easy question I had to begin with.

    Thanks!
    Justin


Bookmarks

Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •