I have an action in my controller called createAction. I also have a model My_Application_Product, that I'm using to create the product within the system. I'm following the Architecting Your Models talk. Is this the "correct" way to save my product? Code for My_Application_Product follows below.
class ProductController extends Zend_Controller_Action {
public function createAction() {
$categoryAdapter = new Application_Model_Categories();
$categories = $categoryAdapter->fetchAll('parent_id IS NOT NULL');
$form = new My_Application_Forms_Product_Create();
$category = $form->getElement('category');
foreach ($categories as $cat) {
$category->addMultiOption($cat->id, $cat->name);
}
if ($this->getRequest()->isPost()) {
if (! $form->isValid($_POST)) {
$this->view->form = $form;
return $this->render('create');
}
$product = new My_Application_Product();
$product->name = $_POST['name'];
$product->company_id = 1;
$product->category_id = $_POST['category'];
$product->trade_names = $_POST['trade_names'];
$product->website = $_POST['website'];
$product->description = $_POST['description'];
$product->closed_loop = $_POST['closed_loop'];
$product->sold_as = $_POST['sold_as'];
$product->sold_in = $_POST['sold_in'];
$product->dilution = $_POST['dilution'];
$id = $product->save();
$url = $this->getHelper('Url')
->url(array('action' => 'registryservices', 'id' => $id));
$this->_redirect($url);
}
$this->view->form = $form;
}
}
'
class My_Application_Product implements My_Application_Product_Interface {
// declare all the internally used variables here.
// if something isn't showing up when trying to save, that's probably
// because it's missing from here
protected $_id;
protected $_name;
protected $_company_id;
protected $_trade_names;
protected $_website;
protected $_description;
protected $_closed_loop;
protected $_sold_as;
protected $_sold_in;
protected $_dilution;
protected $_category_id;
protected $_verification_level;
protected $_dfe_sccp;
protected $_dfe_siicp;
protected $_el_ccd_hsc;
public function __set($name, $value) {
$local_var_name = "_" . $name;
if (property_exists($this, $local_var_name)) {
$this->{$local_var_name} = $value;
}
}
public function __get($name) {
$local_var_name = "_" . $name;
if (property_exists($this, $local_var_name)) {
return $this->{$local_var_name};
}
}
/**
*
* #param array $data The data to save
*/
public function save() {
// this means we're editing something
if ($this->id) {
$table = new My_Application_Product_Table();
$data = $table->find($this->id)->toArray();
$data = $data[0];
foreach (get_class_vars(get_class($this)) as $key => $value) {
if (! is_null($this->$key)) {
$data[preg_replace('/^_/', '', $key)] = $this->$key;
}
}
$id = $table->update($data, sprintf('id = %d', $this->id));
// this means we're creating, and this is the data we need
} else {
$data = array(
'id' => rand(1,1000000),
'name' => $this->name,
'date_created' => date('Y-m-d H:i:s'),
);
$id = $table->insert($data);
}
return $id;
}
}
'
class My_Application_Product_Table extends Zend_Db_Table_Abstract {
protected $_name = 'products';
protected $_primary = 'id';
}
Split your model in multiple classes :
1 class representing the entity (no methods, except for accessors).
this class represents your "real-life" object, and is just a structured data container, which encapsulates data
class My_Application_Model_Product {
protected $_id;
protected $_name;
protected $_company_id;
protected $_trade_names;
protected $_website;
//...
public function __set($name, $value) {
//Implement your setter here
}
public function __get($name) {
}
}
1 class responsible of data mapping.
This class makes is the link between your data source (database, webservice, file...) and your entity.
Class My_Application_Model_DataMapper_Product {
protected $_adapter
public function __construct($adapter)
{
$this->setAdapter($adapter);
}
public function setAdapter($adapter)
{
$this->_adapter = $adapter;
}
public function save(My_Application_Model_Product $product)
{
//Perform your save operation here
}
public function fetchAll()
{
}
public function findById($id)
{
}
//You may implement specific methods for any needed specific operation (search, bulk-update...
}
a third class for data access and persistence (Zend_Db_table, Soap client...) This third class is passed to the datamapper as the adapter and is used inside the methods to getch/save data.
With this architecture, you have a clear separation of responsibilities, and may change one part without affecting the other : for example, you could switch from a database to a webservice without affecting your Product class.
A very simple example is given in the zf Quickstart
Related
I have a JSON file as my database, I wrote this function for creating a new user row into the file:
function create(array $data)
{
$tableFilePath = __DIR__ . "/storage/jsondb/users.json";
$fileData = json_decode(file_get_contents($tableFilePath));
$fileData[] = $data;
$fileDataJson = json_encode($fileData);
file_put_contents($tableFilePath, $fileDataJson);
}
create(['id' => rand(1, 100), 'name' => 'user' . rand(1, 100)]);
The problem is that when I call this function, I expect it inserts 1 row but it inserts 2 rows into the JSON file.
UPDATE:
my JSON file before running the code is an empty array:
[]
After running the code:
[{"id":28,"name":"user-3"},{"id":68,"name":"user-78"}]
as you can see, I executed the code one time, but it inserted two records into the JSON file.
UPDATE-3
I summarized my code because I wanted others can read it easily.
now it's my whole class that I wrote.
here is my interface:
interface CrudInterface
{
public function create(array $data) : int;
public function find(int $id): object;
public function get(array $columns,array $where):array;
public function update(array $data,array $where):int;
public function delete(array $where):int;
}
here I have abstract class BaseModel that implements CrudInterface:
abstract class BaseModel implements CrudInterface
{
protected $connection;
protected $table;
protected $pageSize = 10;
protected $attributes = [];
protected $primaryKey = 'id';
protected function __construct()
{
# if mysql => set mysql connection
}
protected function getAttribute($key)
{
if(!$key || array_key_exists($key, $this->attributes)){
return null;
}
return $this->attributes[$key];
}
}
here JsonBaseModel that extends BaseModel:
class JsonBaseModel extends BaseModel
{
private $dbFolder;
private $tableFilePath;
public function __construct()
{
$this->dbFolder = BASE_PATH . 'storage/jsondb/';
$this->tableFilePath = $this->dbFolder . $this->table . '.json';
}
private function readTable()
{
$fileData = json_decode(file_get_contents($this->tableFilePath));
return $fileData;
}
private function writeTable($fileData){
$fileDataJson = json_encode($fileData);
file_put_contents($this->tableFilePath, $fileDataJson);
}
public function create(array $newData): int
{
$fileData = $this->readTable();
$fileData[] = $newData;
$this->writeTable($fileData);
return $newData[$this->primaryKey];
}
}
and here I have class User that extends JsonBaseModel:
class User extends JsonBaseModel
{
protected $table = 'users';
}
then I made an instance of User class and call it's create() function here:
$data = ['id' => rand(1, 100),'name' => "user-".rand(1, 100)];
$userModel = new User();
$userModel->create($data);
You're passing in an array with the user details, then adding it to the current contents.
If there is already a entry in your file, you'll then have 2 entries.
Make sure that users.json is empty first.
Please, could you help me a bit with my problem?
I have class called Translateable and then clasess Article and Banner, which extend this class.
Problem occurs, when I do this:
$article = (new Article)->find(15);
$banner = (new Banner)->find(1);
$articleTrans = $article->trans(); // method trans is method from Translateable
When I call $article->trans(); I expect output like this:
App\Models\ArticleTrans
Article
but it return this:
App\Models\ArticleTrans
Banner
First row is ok, but the second one if bad and I don't know, how to solve this problem.
I need to have $instance stored as static property.
Could you give me you help?
class Translateable extends Model {
static $transLang = null;
static $transClass = null;
static $instance = null;
public function __construct(array $attributes = array()) {
static::$transLang = App::getLocale();
parent::$transClass = static::$transClass;
parent::$instance = static::$instance;
parent::__construct($attributes);
}
/**
* get items trans
*
* #param null $lang
* #return mixed
*/
public function trans($lang = null) {
if($lang == null) {
$lang = static::$transLang;
}
echo static::$transClass;
echo class_basename(static::$instance);
die();
}
public static function find($primaryKeyVal, $columns = []) {
$tci = new static::$transClass;
$item = static::withTrans()->where(static::$instance->getTable() . '.' . static::$instance->primaryKey, '=', $primaryKeyVal)->where($tci->getTable() . '.lang', '=', static::$transLang)->first();
return $item;
}
}
class Article extends Translateable {
static $transClass = 'App\Models\ArticleTrans';
public function __construct(array $attributes = array()) {
parent::$transClass = static::$transClass;
parent::$instance = $this;
parent::__construct($attributes);
}
}
class Banner extends Translateable {
static $transClass = 'App\Models\BannerTrans';
public function __construct(array $attributes = array()) {
parent::$transClass = static::$transClass;
parent::$instance = $this;
parent::__construct($attributes);
}
}
I'm solving quite strange problem which I'm facing for the first time.
I have some main Class with property static::$someProperty and I extend with this class other classes, e.g. ClassA and ClassB.
Problem is now, when I load
$classA = ClassA
and set there
static::$someProperty = "ClassA"
and echo this value, it works fine and return "ClassA" but then I also load
$classB = ClassB
and set
static::$someProperty = "ClassB"
and when I
echo static::$someProperty
in $classA now, there is value "ClassB".
Do you know, how to solve this problem? Probably it is connected with static, but I don't now, what to do with this.
class Translateable extends Model{
public static $transLang;
public static $transClassInstance;
public static $instance;
public $transInstance = null;
public function __construct(array $attributes = array()) {
self::$transLang = App::getLocale();
$tcName = static::$instance->transClass;
static::$transClassInstance = new $tcName;
parent::__construct($attributes);
}
/**
* add trans to the item
*
* #return mixed
*/
public static function withTrans($lang = null) {
if($lang == null) {
$lang = static::$transLang;
}
return static::join(static::$transClassInstance->getTable(), function ($join) use ($lang) {
$join->on(static::$instance->getTable() . '.' . static::$instance->primaryKey, '=', static::$transClassInstance->getTable() . '.' . static::$instance->primaryKey)->where(static::$transClassInstance->getTable() . '.lang', '=', $lang);
})->where(static::$transClassInstance->getTable() . '.lang', '=', $lang)
;
}
}
class Nested extends Translateable{
// protected $lft, $lvl, $rgt, $parent_ID;
public static $transClassInstance;
public static $transLang;
public function __construct(array $attributes = array()) {
self::$transLang = App::getLocale();
$tcName = static::$instance->transClass;
static::$transClassInstance = new $tcName;
parent::$instance = $this;
parent::__construct($attributes);
}
/**
*
* get $this item child
*
* #return null
*/
public function getChilds() {
$primaryKeyName = $this->primaryKey;
$parent_id = $this->$primaryKeyName;
// here is echo PageTrans instead of ProductCategoryTrans
echo static::$transClassInstance->getTable().'<br/>';
echo static::$transClassInstance->getTable() . '.lang'.'<br/>';
$query = static::where('parent_ID', '=', $parent_id)->where(static::$transClassInstance->getTable() . '.lang', '=', static::$transLang);
echo $query->toSql();
$this->generateItemsQuery($query);
$query->orderBy('lft', 'ASC');
$categories = $query->get();
return $categories;
}
}
class ProductCategory extends Nested{
public $transClass = 'App\Models\ProductCategoryTrans';
public function __construct(array $attributes = array()) {
static::$instance = $this;
parent::__construct($attributes);
}
}
class Page extends Nested{
public $transClass = 'App\Models\PageTrans';
public function __construct(array $attributes = array()) {
static::$instance = $this;
parent::__construct($attributes);
}
}
Example usage:
// find product category with ID == 1
$productCategory = (new ProductCategory)->find(1); // "ClassA"
// get some page...
$page = (new Page)->find(1); // find page with ID == 1 // "ClassB"
// get childs of loaded category
$categoryChilds = $productCategory->getChilds(); // get this category
Try to use self in classA and classB
self::$someProperty = 'test';
I'm trying to persist a domain model to a DB without using an ORM just for fun.
It's pretty easy to persist properties, but I'm having hard time persisting a collection.
Let's say I have the following two objects.
class aModel
{
private $items = [];
public function __construct($id, $name, array $items = [])
{
$this->id = $id;
$this->name = $name;
$this->items = $items;
}
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
public function addItem(Item $item)
{
$this->items[] = $item;
}
}
class aDBRepository
{
public function persist(aModel $aModel)
{
$attributes = [
'id' => $aModel->getId(),
'name' => $aModel->getName()
];
$this->table->insert($attributes);
}
}
// Code
$aModel = new aModel("test id", "a name");
$aModel->addItem(new Item("id", "name"));
When I create a new aModel and add a new item to it, how do I detect 'unsaved' items and persist them?
I can only think of adding isSaved method in the Item class and loop through $items variable in aModel.
Without using reflection, what's the best way?
There are lots of articles regarding factory method implementation in PHP.
I want to implement such a method for my MongoDB implementation in PHP.
I wrote the code something like below. Please Look at that code.
<?php
class Document {
public $value = array();
function __construct($doc = array()) {
$this->value = $doc;
}
/** User defined functions here **/
}
class Collection extends Document {
//initialize database
function __construct() {
global $mongo;
$this->db = Collection::$DB_NAME;
}
//select collection in database
public function changeCollection($name) {
$this->collection = $this->db->selectCollection($name);
}
//user defined method
public function findOne($query = array(), $projection = array()) {
$doc = $this->collection->findOne($query, $projection);
return isset($doc) ? new Document($doc) : false;
}
public function find($query = array(), $projection = array()) {
$result = array();
$cur = $this->collection->find($query, $projection);
foreach($cur as $doc) {
array_push($result, new Document($doc));
}
return $result;
}
/* Other user defined methods will go here */
}
/* Factory class for collection */
class CollectionFactory {
private static $engine;
private function __construct($name) {}
private function __destruct() {}
private function __clone() {}
public static function invokeMethod($collection, $name, $params) {
static $initialized = false;
if (!$initialized) {
self::$engine = new Collection($collection);
$initialized = true;
}
self::$engine->changeCollection($collection);
return call_user_func_array(array(self::$engine, $name), $params);
}
}
/* books collection */
class Books extends CollectionFactory {
public static function __callStatic($name, $params) {
return parent::invokeMethod('books', $name, $params);
}
}
/* authors collection */
class Authors extends CollectionFactory {
public static function __callStatic($name, $params) {
return parent::invokeMethod('authors', $name, $params);
}
}
/* How to use */
$books = Books::findOne(array('name' => 'Google'));
$authors = Authors::findOne(array('name' => 'John'));
Authors::update(array('name' => 'John'), array('name' => 'John White'));
Authors::remove(array('name' => 'John'));
?>
My questions are:-
Is this correct PHP implementation of Factory method?
Does this implementation have any issues?
Are there any better methodologies over this for this scenario?
Thanks all for the answers.
Hmm no, because with your piece of code you make ALL methods on the collection class available for a static call. That's not the purpose of the (abstract) factory pattern.
(Magic) methods like __callStatic or call_user_func_array are very tricky because a developer can use it to call every method.
What would you really like to do? Implement the factory pattern OR use static one-liner methods for your MongoDB implementation?!
If the implementation of the book and author collection has different methods(lets say getName() etc..) I recommend something like this:
class BookCollection extends Collection {
protected $collection = 'book';
public function getName() {
return 'Book!';
}
}
class AuthorCollection extends Collection {
protected $collection = 'author';
public function getName() {
return 'Author!';
}
}
class Collection {
private $adapter = null;
public function __construct() {
$this->getAdapter()->selectCollection($this->collection);
}
public function findOne($query = array(), $projection = array()) {
$doc = $this->getAdapter()->findOne($query, $projection);
return isset($doc) ? new Document($doc) : false;
}
public function getAdapter() {
// some get/set dep.injection for mongo
if(isset($this->adapter)) {
return $this->adapter;
}
return new Mongo();
}
}
class CollectionFactory {
public static function build($collection)
{
switch($collection) {
case 'book':
return new BookCollection();
break;
case 'author':
return new AuthorCollection();
break;
}
// or use reflection magic
}
}
$bookCollection = CollectionFactory::build('book');
$bookCollection->findOne(array('name' => 'Google'));
print $bookCollection->getName(); // Book!
Edit: An example with static one-liner methods
class BookCollection extends Collection {
protected static $name = 'book';
}
class AuthorCollection extends Collection {
protected static $name = 'author';
}
class Collection {
private static $adapter;
public static function setAdapter($adapter) {
self::$adapter = $adapter;
}
public static function getCollectionName() {
$self = new static();
return $self::$name;
}
public function findOne($query = array(), $projection = array()) {
self::$adapter->selectCollection(self::getCollectionName());
$doc = self::$adapter->findOne($query, $projection);
return $doc;
}
}
Collection::setAdapter(new Mongo()); //initiate mongo adapter (once)
BookCollection::findOne(array('name' => 'Google'));
AuthorCollection::findOne(array('name' => 'John'));
Does it make sense for Collection to extend Document? It seems to me like a Collection could have Document(s), but not be a Document... So I would say this code looks a bit tangled.
Also, with the factory method, you really want to use that to instantiate a different concrete subclass of either Document or Collection. Let's suppose you've only ever got one type of Collection for ease of conversation; then your factory class needs only focus on the different Document subclasses.
So you might have a Document class that expects a raw array representing a single document.
class Document
{
private $_aRawDoc;
public function __construct(array $aRawDoc)
{
$this->_aRawDoc = $aRawDoc;
}
// Common Document methods here..
}
Then specialized subclasses for given Document types
class Book extends Document
{
// Specialized Book functions ...
}
For the factory class you'll need something that will then wrap your raw results as they are read off the cursor. PDO let's you do this out of the box (see the $className parameter of PDOStatement::fetchObject for example), but we'll need to use a decorator since PHP doesn't let us get as fancy with the Mongo extension.
class MongoCursorDecorator implements MongoCursorInterface, Iterator
{
private $_sDocClass; // Document class to be used
private $_oCursor; // Underlying MongoCursor instance
private $_aDataObjects = []; // Concrete Document instances
// Decorate the MongoCursor, so we can wrap the results
public function __construct(MongoCursor $oCursor, $sDocClass)
{
$this->_oCursor = $oCursor;
$this->_sDocClass = $sDocClass;
}
// Delegate to most of the stock MongoCursor methods
public function __call($sMethod, array $aParams)
{
return call_user_func_array([$this->_oCursor, $sMethod], $aParams);
}
// Wrap the raw results by our Document classes
public function current()
{
$key = $this->key();
if(!isset($this->_aDataObjects[$key]))
$this->_aDataObjects[$key] =
new $this->sDocClass(parent::current());
return $this->_aDataObjects[$key];
}
}
Now a sample of how you would query mongo for books by a given author
$m = new MongoClient();
$db = $m->selectDB('test');
$collection = new MongoCollection($db, 'book');
// search for author
$bookQuery = array('Author' => 'JR Tolken');
$cursor = $collection->find($bookQuery);
// Wrap the native cursor by our Decorator
$cursor = new MongoCursorDecorator($cursor, 'Book');
foreach ($cursor as $doc) {
var_dump($doc); // This will now be an instance of Book
}
You could tighten it up a bit with a MongoCollection subclass and you may as well have it anyway, since you'll want the findOne method decorating those raw results too.
class MongoDocCollection extends MongoCollection
{
public function find(array $query=[], array $fields=[])
{
// The Document class name is based on the collection name
$sDocClass = ucfirst($this->getName());
$cursor = parent::find($query, $fields);
$cursor = new MongoCursorDecorator($cursor, $sDocClass);
return $cursor;
}
public function findOne(
array $query=[], array $fields=[], array $options=[]
) {
$sDocClass = ucfirst($this->getName());
return new $sDocClass(parent::findOne($query, $fields, $options));
}
}
Then our sample usage becomes
$m = new MongoClient();
$db = $m->selectDB('test');
$collection = new MongoDocCollection($db, 'book');
// search for author
$bookQuery = array('Author' => 'JR Tolken');
$cursor = $collection->find($bookQuery);
foreach($cursor as $doc) {
var_dump($doc); // This will now be an instance of Book
}