Skip to content
This repository

Database - Row Factory #805

Closed
wants to merge 1 commit into from

5 participants

Jan Dolecek Jan-Sebastian Fabík David Grudl Filip Procházka Jan Škrášek
Jan Dolecek

This would allow to create model classes easily.

I've got a database layer where I need to have some logic, mostly for traversing the database. For that reason, I'd like to have a class for each db table (extending ActiveRow).

Jan Dolecek

I heard some rumors that this is bad, but I couldn't find any reasons nor a discussion. If you think this is wrong, pls tell me why. Thx

Jan-Sebastian Fabík

:+1:

I use the same solution in my library and I don't think it's bad.

I also think that it is a good idea to add a default row factory (similar to this) and add it to NetteExtension so that it could be configured very easily in config.neon. It might be also useful to allow getters/setters in ActiveRow (like this way).

After that it could be used like this:

nette:
    database:
        default:
            dsn: '%database.driver%:host=%database.host%;dbname=%database.dbname%'
            user: %database.user%
            password: %database.password%

            rowClasses: { # entities
                articles: Model\Article
                users: Model\User
                # table name => row class
            }


services: # repositories
    articles: Model\Articles
    users: Model\Users
namespace Model;
use Nette\Database\Table\ActiveRow;

class Article extends ActiveRow
{
}
namespace Model;
use Nette\Database\Table\ActiveRow;

class User extends ActiveRow
{
    public function getFullname()
    {
        return "$this->firstname $this->surname";
    }
}
namespace Model;
use Nette\Database\Connection,
    Nette\Object;

abstract class Table extends Object
{
    /** @var Nette\Database\Connection */
    protected $connection;



    public function __construct(Connection $connection)
    {
        $this->connection = $connection;
    }



    protected function getTable()
    {
        return $this->connection->table($this->name);
    }



    /** @return Nette\Database\Table\ActiveRow|FALSE */
    public function find($id)
    {
        return $this->getTable()->find($id);
    }
}
namespace Model;

class Articles extends Table
{
    protected $name = 'articles';
}
namespace Model;

class Users extends Table
{
    protected $name = 'users';
}
echo $users->find(123)->fullname;
David Grudl
Owner
dg commented October 11, 2012

Yes, it is bad idea.

What means class Model\User extends ActiveRow? It means User is some kind of database row. Actually, User is an entity, which can be stored in database.

David Grudl
Owner
dg commented October 11, 2012

Better idea is to add support for "something like extension methods", but defined per table.

Jan Dolecek

Right, it's "bad". But for me it was good enough and simple to use.
Actually, this is exactly the same as ActiveRecord in rails or Zend Db, right? Which means only that rails and zend are bad :D

Extension methods sound like a good idea, I'll try playing with that.

David Grudl
Owner
dg commented October 11, 2012

Yes, Rails are absolute bastl!

Jan-Sebastian Fabík

@HosipLan I think it is bad for the same reason. (class Entity extends Table\ActiveRow - An entity isn't some kind of a database row.)

@dg What do you think about this approach:

  1. I will create an entity (interface Model\IArticle). I will use the interface Model\IArticle for article entities everywhere in my app.

    interface IArticle
    {
        /** @return int */
        function getId();
    
        /**
         * @param  string
         * @return string
         */
        function getTitle($lang);
    
        /**
         * @param  string
         * @param  string
         */
        function setTitle($lang, $title);
    
        /**
         * @param  string
         * @return string
         */
        function getContent($lang);
    
        /**
         * @param  string
         * @param  string
         */
        function setContent($lang, $content);
    
        /** @return Model\IComment[]|Iterator */
        function getComments();
    
        // ...
    }
    
  2. I will implement these methods in the Article class. (Maybe it would be also better to rename Model\Article to ArticleRow, so it will be clear that it's a row.)

    class ArticleRow extends ActiveRow implements IArticle
    {
        public function getId()
        {
            return $this->id;
        }
    
        public function getTitle($lang)
        {
            return $this->{"title_$lang"};
        }
    
        public function setTitle($lang, $title)
        {
            $this->{"title_$lang"} = $title;
        }
    
        public function getContent($lang)
        {
            return $this->{"content_$lang"};
        }
    
        public function setContent($lang, $content)
        {
            $this->{"content_$lang"} = $content;
        }
    
        public function getComments()
        {
            return $this->related('comments', 'article_id');
        }
    
        // ...
    }
    
Jan-Sebastian Fabík

And I can do the same thing with tables.

  1. I will create a repository:

    interface IArticleRepository
    {
        /** @return Model\IArticle|FALSE */
        function get($id);
    
        /** @return Model\IArticle[]|Iterator */
        function getAll();
    
        /** @return void */
        function save(IArticle $article);
    
        // ...
    }
    
  2. I will implement these methods in the class Articles (It can be also renamed to ArticlesTable, so it will be clear that it's a database table):

    class ArticlesTable extends Table implements IArticleRepository
    {
        public function get($id)
        {
            return $this->getTable()->find($id);
        }
    
        public function getAll()
        {
            return $this->getTable();
        }
    
        public function save(IArticle $article)
        {
            if (!$article instanceof ArticleRow) {
                throw new InvalidArgumentException("An instance of class Model\ArticleRow was expected.");
            }
            $article->save();
        }
    }
    

I think this approach is clear. For example, I can define my own article repository for a testing purpose.

Jan Škrášek
Collaborator

Well, here we are... @juzna suggests fully functional pragmatic sollution, but @dg is against because some theoreticly problematic parts. As the result we are all in shit, because no solution is better than working solution. Same as #469 - nette has no CSRF protection on get request. Our greatest WIN!

Jan-Sebastian Fabík

@hrach The solution suggested by @juzna is, for example, difficult to test. But if you extend it with a few lines of code (as I proposed above), you get a solution that is fully functional and also theoretically correct.

Or am I wrong? Is the solution bad for another reason?

Jan Škrášek
Collaborator

@fabik Hey, @juzna din't suggested any solution, he just gave example. I agree, your approach is much better :) Btw, sth similar I'm already using (not talking about ndab).

Jan-Sebastian Fabík

@dg I will create a pull request, if you consider this solution good.

Jan Dolecek

@fabik pls post link to yout branch where this is possible and I'll be happy to play with it.
Do you have real life experience with this approach? I've been using my bastl for a while and it's pretty easy to use, though I already encountered some problems with this approach - I need to create entities from external source (not database) and work with them, which is not possible at the moment.

Filip Procházka

@fabik I didn't see that ;)

ORM Entity !== Table Row Building ORM on top of Nette\Database can't be done by creating entities from rows. You're creating Table/Row Gateway, which might be good to, but it's not an ORM.

You can't build stuff unless you we all understand each other. Decide what you wanna build and use right terms.

Jan-Sebastian Fabík

@juzna I will create a branch if it is considered good. You can use fabik/database until then. I cannot imagine using Nette\Database without that as it simplified my templates a lot. The solution that uses interfaces allows creating entities from an external source.

@HosipLan I think a row gateway (ArticleRow) can be also a kind of an entity (IArticle) as it has some attributes (that are loaded from columns in the case of ActiveRow) and some logic (defined in the gateway). And a table gateway (ArticlesTable) can be also a kind of a repository (IArticleRepository) as it loads and stores these entities in a data source (a database in the case of ArticlesTable).

Jan Škrášek
Collaborator

Since there are possibilities how to "hack" this, could be this issue closed?

Jan-Sebastian Fabík

I think the solution that I am currently using is absolutely clean. It can be easily replaced with another storage and mocked in unit tests. Does anyone disagree? So what's the problem?

Jan Škrášek hrach closed this March 24, 2013
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Showing 1 unique commit by 1 author.

Oct 06, 2012
Jan Dolecek Database: database extensible by row factory 10771d1
This page is out of date. Refresh to see the latest.
20  Nette/Database/Connection.php
@@ -43,6 +43,9 @@ class Connection extends PDO
43 43
 	/** @var Nette\Caching\Cache */
44 44
 	private $cache;
45 45
 
  46
+	/** @var IRowFactory */
  47
+	private $rowFactory;
  48
+
46 49
 	/** @var array of function(Statement $result, $params); Occurs after query is executed */
47 50
 	public $onQuery;
48 51
 
@@ -119,6 +122,23 @@ public function getCache()
119 122
 
120 123
 
121 124
 
  125
+	public function setRowFactory(IRowFactory $rowFactory)
  126
+	{
  127
+		$this->rowFactory = $rowFactory;
  128
+	}
  129
+
  130
+
  131
+
  132
+	/**
  133
+	 * @return IRowFactory
  134
+	 */
  135
+	public function getRowFactory()
  136
+	{
  137
+		return $this->rowFactory;
  138
+	}
  139
+
  140
+
  141
+
122 142
 	/**
123 143
 	 * Generates and executes SQL query.
124 144
 	 * @param  string  statement
31  Nette/Database/IRowFactory.php
... ...
@@ -0,0 +1,31 @@
  1
+<?php
  2
+
  3
+/**
  4
+ * This file is part of the Nette Framework (http://nette.org)
  5
+ *
  6
+ * Copyright (c) 2004 David Grudl (http://davidgrudl.com)
  7
+ *
  8
+ * For the full copyright and license information, please view
  9
+ * the file license.txt that was distributed with this source code.
  10
+ */
  11
+
  12
+namespace Nette\Database;
  13
+
  14
+use Nette;
  15
+
  16
+
  17
+
  18
+/**
  19
+ * Information about tables and columns structure.
  20
+ */
  21
+interface IRowFactory
  22
+{
  23
+	/**
  24
+	 * Create new entity
  25
+	 *
  26
+	 * @param  array
  27
+	 * @param  Table\Selection
  28
+	 * @return mixed
  29
+	 */
  30
+	function createRow(array $data, Table\Selection $table);
  31
+}
7  Nette/Database/Table/Selection.php
@@ -493,7 +493,12 @@ protected function execute()
493 493
 
494 494
 	protected function createRow(array $row)
495 495
 	{
496  
-		return new ActiveRow($row, $this);
  496
+		if ($factory = $this->connection->getRowFactory()) {
  497
+			return $factory->createRow($row, $this);
  498
+
  499
+		} else {
  500
+			return new ActiveRow($row, $this);
  501
+		}
497 502
 	}
498 503
 
499 504
 
Commit_comment_tip

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.