I'm currently working on a content management system in Laravel 4.
The current situation is that a Page class has many Item classes. Plain and simple.
As time passed, the Item class grew in size where we implemented new features or data an Item could store. At this point, an Item can contain information for -containers, plain text, images, navigation, besides "shared" data that every item always has, eg. position, is_visible, etc.
Now my belief is that the current state is unmanagable, so i started thinking of deviding the Item class into multiple "inheriting" classes (as a C# developers background), like this:
Item.php
id
position
isVisible
isCommentable
Container.php inherits Item.php
bgColor
bgImage
size
Text.php inherits Item.php
header
content
(So again, all this from a C# point of view).
Now, since I'm working in Laravel, having the Item class inheriting from Eloquent, I thought the proper solution is to bind Container/Text classes to the Item class by morphMany and morphTo functionallity. The wall I'm hitting is this one: should I make choose to "create" a Text object: how would I ensure that the "base data" in the Item-class gets filled aswell.
When setting up the system, I'm working with migrations to fill the first records.
Text.php
class Text extends Eloquent {
protected $fillable = [];
public function items()
{
return $this->morphMany('Item', 'itemable');
}}
Item.php
class Item extends Eloquent
{
protected $table = "items";
public function page()
{
return $this->belongsTo('Page');
}
public function itemable()
{
return $this->morphTo();
}
}
I will spare you the table setup migrations, it's the insert that i'm interested in:
public function up()
{
DB::table('containers')->insert(array(
'bgColor'=>'#ffebcf',
'bgImage'=>'img/header-jumbotron.png',
'bgImageRepeat'=>'repeat',
'bgPosition'=>'left top',
));
}
When executing this, offcourse, it won't have any information for the initial Item object. But this should be required because without this information, the Container object shouldn't excist at all. Maybe my thinking is a little weird, or I'm missing something that is very very obvious. I'm looking for a neat solution that fits my 'thoughtproces' - can't find it! At this time: the wall has struck.
Also - I would like some feedback as to my approach. Is this the right thing to do in the first place? Appreciated!
Related
I'm reworking a project on Laravel 5.1
What I realize is that the old classes have become much complicated and do not really follow the 'single responsibility' principle anymore.
So I'm planning to do such:
<?php
class User extends Model
{
}
class SocialUser extends User
{
}
So I have a few questions,
Is it possible to achieve that?
If yes, then does the SocialUser class link back to the same database table which is Users and would it conflict with the User model itself?
Is this all a good design practice at the first place? Or I better make use of traits?
Thank you.
What you’re doing (extending the User model) is perfectly fine, and an approach I use myself in projects.
For example, if an application I’m building has shop-like functionality, then I may create a Customer model that extends my User model, and contains say, order-related relations:
class Customer extends User
{
public function orders()
{
return $this->hasMany(Order::class, 'customer_id');
}
public function worth()
{
return $this->orders()->sum(function ($order) {
return $order->total();
});
}
}
In a recent project, I’ve been working on email campaign functionality and created a Recipient class that extends the User model to add campaign-related methods:
class Recipient extends User
{
public function campaigns()
{
return $this->belongsToMany(Campaign::class, 'recipient_id');
}
}
Because both of these classes extend the User model, I get all of those (and Eloquent) methods:
$customers = Customer::with('orders')->get();
So long as you set the table in your base User model, any classes that inherit it will use that same table, even though the model may be named differently (i.e. Customer, Recipient, Student etc).
IMHO I would go for the Repository pattern. It make's a lot of sense in your situation.
I would do the following:
interface UserRepository {
public function find($id);
public function getAll();
public function create(array $attributes);
public function destroy($id);
//you get the point
}
class CoreUserRepository implements UserRepository
{
//implement the interface rules
}
class SocialUserRepository extends CoreUserRepository
{
//implement the specific logic related to a SocialUser
}
Update
As Mjh described in the comments simply implementing the interface on all UserTypeRepository caused repetition - probably not what you want!
By extending your CoreUser you avoid repetition & maintain a design that will work for your situation.
Although, in your case it could be argued that you are still following SRP because everything in the User model is relating to a user, it's only the type of user which is differing.
Why go for the Repository Pattern?
You are ensuring you have a contractual agreement that all User
Repositories need to implement.
Code is easier to maintain.
Business and data access logic can be tested separately
Should you extend your User model?
Here you are in danger of model pollution. While you can do anything with a model - not everything is a good idea.
Defining relationships on this approach would be a headache due to the confusion caused.
I'm learning Yii. I have a test development which contains a number of tables (employee, personalDetails, address). My understanding of MVC leads me to see these almost as individual planets, where each (MVC) component plays a clearly defined role within that world.
I have a question that’s starting to bug me because I now want to pass requests for data and calculations between these worlds. I have come across a few postings on how to do this, but they appear more like “hacks” than “prescribed” practises. I’m obviously keen not to pick up bad habits. This kind of process is obviously a root requirement of any development so would like to ask for some guidance on this.
A specific example would be to return a view of employees who have take home salaries > $100,000 including bonuses ( e.g. employee controller asks personalDetails controller to calculate {goss salary + bonuses – tax} and return all appropriate instances, it then looks up and returns the relevant employees).
So do I create a function in personalDetails and call it from inside employee controller, or should this kind of thing go in an extension ... or is there another approach?
I’d appreciate your guidance on this
For encapsulated self managed view parts use widgets. For above case you could create widget with configurable treshhold.
If you have to ask different controller to calculate something it is a bad practice. Place such calculations in model instead. Model is highly reusable, view can be reused, however controller should only respond to action and bind data to view.
Fat model, thin controller, wise view.
Here is some draft code:
First create model with any needed calculations:
class Employee extends CActiveRecord
{
public function getTotalSalary()
{
// Do any calculations here
// ...
return $salary;
}
}
Then you can reuse it in controllers:
class FirstController extends CController
{
public function actionPersonDetails()
{
$model = $this->_loadModel();
// By assigning this you will have getTotalSalary() available in view
$this->render('personDetails', ['model' => $model]);
}
}
class SecondController extends CController
{
public function actionViewSallary()
{
$model = $this->_loadModel();
// Also here you will have getTotalSalary() available in view
$this->render('viewSallary', ['model' => $model]);
}
}
And for more complex scenarios where you need something standalone create widget:
class EmployeesWidget extends CWidget
{
public $minSalary = 0;
private $_data = null;
public function init()
{
$this->_data = new CActiveDataProvider(/* Criteria here with $this->minSalary as param */);
}
public function run()
{
$this->render('employeesWidget', ['data' => $this->_data]);
}
}
Then you can easy use it in any view, even in other widgets:
$this->widget('path.to.EmployeesWidget', [
'minSallary' => 10000
]);
i'm working with Agile Toolkit
i got a Model_Product
class Model_Product extends Model_Table {
public $table="product";
function init(){
parent::init();
$this->addField('name')->mandatory(true);
$this->addField('price')->mandatory(true)->type('money');
$this->addField('user_id')->refModel('Model_User')
->defaultValue($this->api->auth->get('id'));
//$this->hasOne('User',null,'email'); => send me an error message
}
}
and Model_User
class Model_User extends Model_Table {
public $table="user";
function init(){
parent::init();
$this->addField('first_name')->mandatory('Prénom nécesssaire');
$this->addField('last_name')->mandatory('Nom nécesssaire');
$this->addField('email')->mandatory('entrez un email valide');
$this->addField('nationality')->mandatory('nécessaire')->enum(array('FR','EN','US'));
$this->addField('birthday')->defaultValue(date('Y-m-d'))->type('date');
$this->addField('is_admin')->type('boolean');
$this->hasMany('Product','user_id');
}
I want to list on a User page all the products from one User
$q=$this->api->db->dsql();
$q->table('product')->where('product.user_id',$this->api->auth->model['id']);
$tab->add('GRID')->setModel($q);
Some way, I get it wrong because I get an error no mater how I try to filter my Model.
If you're not using newest ATK4 version from Github then you should grab it and stay up-to-date.
You should do like this:
1) In Model_Product create hasOne reference and not refModel (it's deprecated).
// adding 'user_id' parameter is not needed, it'll be calculated anyway
// but many developers add it anyway to clear thing up a bit.
$this->hasOne('User','user_id')->defaultValue($this->api->auth->get('id'));
2) Model_User is OK.
Just some side-notes about it:
I don't think you should make birthday = today() by default.
It's quite unbelievable that child at his first day in this world will use computer :)
is_admin should be mandatory + defaultValue(false) - by default user is not admin.
3) How to list all all products from current user.
// model of current user
$model = $this->add('Model_User')
->load($this->api->auth->get('id'));
// add grid
$page->add('Grid')
// reference Product model with condition already set
->setModel($model->ref('Product'));
and that's it.
Maybe even better and safer way is to define new model class for logged in user:
class Model_Myself extends Model_User {
function init(){
parent::init();
$this->addCondition('id', $this->api->auth->get('id'));
$this->loadAny(); // I'm not sure do we have to explicitly load it here
}
}
and then create grid like this
// model of products of current user
$prod_model = $this->add('Model_Myself')->ref('Product');
// add grid
$page->add('Grid')->setModel($prod_model);
I have a model called Task defined like this (fields not relevent to question removed)
<?php
class Model_Task extends Model_Table {
public $entity_code='vscrum_task';
public $table_alias='tk';
function init(){
parent::init();
// debug causes error in Ajax in ATK v4.1.1
// $this->debug(true);
$this->addField('id')->system(true)->visible(false);
$this->addField('task_desc')->mandatory(true)->visible(true);
$this->addField('tasktype_id')->mandatory(true)->refModel('Model_TaskType');
$this->addField('team_id')->system(true)->visible(false);
and the refModel tasktype is defined like this (fields not relevent to question removed)
<?php
class Model_TaskType extends Model_Table {
public $entity_code='vscrum_tasktype';
public $table_alias='ty';
function init(){
parent::init();
$this->addField('id')->mandatory(true);
$this->addField('name')->mandatory(true);
$this->addField('team_id');
}
}
I have a CRUD which is based on task and is now (thanks to help from Jancha and Romans on stackoverflow) is working fine.
I want to limit the options in the drop down for TaskType to only those tasktypes defined for the user's team. I tried putting an addCondition in the TaskType Model referencing a session variable i had previously memorized
$this->addCondition('team_id',$p->api->recall('team_id'));
and also using a direct call to a value for the logged in use
$this->addCondition('team_id',$p->api->auth->get('team_id'));
but this results in showing the Tasktype fine in the Grid
but leaves it empty for both Edit and Add in the Ajax dialog.
If i remove the addCondition line from the TaskType Model, it shows all values in the list but i will always want this restricted to a subset.
As this is the referred Model and not the Model that the CRUD is based on, any suggestions on how i get this to work as expected ?
I tried Roman's suggestion of having a model which is the TaskType and a new model extended from that which is the TaskType_Team with the addCondition in it like this
class Model_TaskType_Team extends Model_TaskType {
function init(){
parent::init();
$this->addCondition('team_id',$p->api->auth->get('team_id'));
}
for which i needed to create a subdirectory undel Model called TaskType otherwise it didnt find the new Model but the end result is the same. I think this is related to another issue i previously had where the Ajax dialog loses access to $p->api and so doesnt display the restriction (and this is why it works fine for the grid on the same page as that isnt in an ajax dialog but i dont want to use a stickyGet to resolve this for security (dont want to be able to modify the URL to see other teams data) and session variables ($p->auth->memorise and $p->auth->recall) also dont seem work in this case - any further suggestions ?
Remember that you can extend your models like that. In fact, this is very often used in larger projects.
class Model_TaskType_Team extends Model_TaskType {
function init(){
parent::init();
$this->addCondition('team_id',$this->api->auth->get('team_id'));
}
}
I have a controller/model for projects. so this controls the projects model, etc, etc. I have a homepage which is being controlled by the pages_controller. I want to show a list of projects on the homepage. Is it as easy as doing:
function index() {
$this->set('projects', $this->Project->find('all'));
}
I'm guessing not as I'm getting:
Undefined property: PagesController::$Project
Can someone steer me in the right direction please,
Jonesy
You must load every model in the controller class by variable $uses, for example:
var $uses = array('Project');
or in action use method
$this->loadModel('Project');
In my opinion the proper way to do this is add a function to your current model which instantiates the other model and returns the needed data.
Here's an example which returns data from the Project model in a model called Example and calls the data in the Example controller:
Using Project Model inside Example Model:
<?php
/* Example Model */
App::uses('Project', 'Model');
class Example extends AppModel {
public function allProjects() {
$projectModel = new Project();
$projects = $projectModel->find('all');
return $projects;
}
}
Returning that data in Example Controller
// once inside your correct view function just do:
$projects = $this->Example->allProjects();
$this->set('projects', $projects);
In the Example view
<?php
// Now assuming you're in the .ctp template associated with
// your view function which used: $projects = $this->Example->allProjects();
// you should be able to access the var: $projects
// For example:
print_r($projects['Project']);
Why is this "better" practice than loading both models into your controller? Well, the Project model is inherited by the Example model, so Project data now becomes part of the Example model scope. (What this means on the database side of things is the 2 tables are joined using SQL JOIN clauses).
Or as the manual says:
One of the most powerful features of CakePHP is the ability to link relational mapping provided by the model. In CakePHP, the links between models are handled through associations.
Defining relations between different objects in your application should be a natural process. For example: in a recipe database, a recipe may have many reviews, reviews have a single author, and authors may have many recipes. Defining the way these relations work allows you to access your data in an intuitive and powerful way. (source)
For me it's more reasonable to use requestAction. This way the logic is wrapped in the controller.
In example:
//in your controller Projects:
class ProjectsController extends AppController {
function dashboard(){
$this->set('projects', $this->Project->find('all'));
}
$this->render('dashboard');
}
Bear in mind that you need to create dashboard.ctp in /app/views/projects of course.
In the Page's dashboard view (probably /app/views/pages/dashboard.ctp) add:
echo $this->requestAction(array('controller'=>'projects', 'action'=>'dashboard'));
This way the logic will remain in the project's controller. Of course you can request /projects/index, but the handling of the pagination will be more complicated.
more about requestAction(). but bear in mind that you need to use it carefully. It could slow down your application.