I understand the question title seems quite open-ended but I have already done a bit of reading/experimenting around the subject and have got somewhere with Cake in addition to building my own procedural-style app. My aim is to bring over my procedural app to CakePHP.
What the App does (or wants to do)
So, I have a bunch of clients, each client has 1 or more accounts. Each account has holdings that whose value is stored for a specific date. Here's an example of the database structure
The idea is to have the standard CRUD pages for clients, accounts etc. But also to have more sophisticated pages, for example, to give a return history for a clients accounts along with portfolio statistics. (I have other tables with more info in them but for the sake of simplicity I have omitted them from this issue for the time being)
What I've done
So, having following the tutorials, I've made models for clients and accounts and managed to link (associate?) the clients and accounts tables in CakePHP. I've also made controllers and views that allow me to retrieve the clients and accounts and made index, view, edit views.
Here are my model classes:
//Client.php
class Client extends AppModel {
public $hasMany = 'Account';
}
//Account.php
class Account extends AppModel {
public $belongsTo = 'Client';
public $hasMany = 'Holding';
}
//Holding.php
class Holding extends AppModel {
public $belongsTo = 'Account';
}
What I did Before
Let's say I want to make a page that shows the portfolio return for a given client. What I did in my last (crude) app was just make a php file (portfolio.php). I would then query the database for sum of the client's holdings on each month-end using a raw SQL statement. Then I'd loop though the array of returns calculating the % change in value from one month to the next.
This data can then be presented in an html table and I could then even plot the values on a chart etc.
This is a simplistic example of course. This page/view would introduce other info like benchmark comparisons (via another database table), stats like Sharpe ratio, vol, annualized return etc.
My question
How would similar functionality to the last section be implemented in CakePHP? It doesn't seem the view/page I'm after fits into the MVC ideology. Where would the logic involved in doing calculations go (e.g. calculating returns from the array of values)?
If I could have an explanation of how this kind of thing is implemented in MVC/CakePHP it would be really helpful.
The blog tutorial makes loads of sense but I can't yet see how it would extend to more complex applications.
CakePHP as a framework lets you quite free of where you put your logic. It tells you where you should make your DB call (models) and where you should control the flow of your app (controllers) but when it comes to behind the scene logic, the docs doesn't say much.
MVC as I understand it on the other hand is quite clear
Everything that treats data should go in the model.
Everything that deals with user interaction/flow control should go in the controller.
Everything that deals with user interface should go in the view.
Applied to CakePHP
That means most of your logic remains in the Models, and very few lays in the Controllers. It's also better this way since you can easily call $this->Model->foo() from any linked controller whereas calling a function defined in a another controller is hardly encouraged due to performance issue and is really not a good practice.
Other things you can investigate are Components and Behaviors.
Components are basically a set a functions that you want to share between controllers. It has more or less the same capabilities than the Controller but does not have dedicated routes, views or model.
Behaviors are the equivalent for the models, its good when you have several models that have similar behavior and you want to keep you code DRY.
Your case
In your particular case, I would go with small functions in models. For instance, all the functions that manipulates holdings information should be in the Holding model. It's nice because you can call these functions both from the Controller or another model, you could have a preparePortfolio() function in your User model that calls a few functions in the Holding model, do all kind of arrangement and then give back consolidated data to the UserController ready to be passed to the view.
One last thing
I highly recommend you have a look at the Hash class if you deal with arrays and loops. You can avoid looping through multidimensional array by using some of this functions and it can literally boost your performance if you deal with huge arrays. It has also a much cleaner syntax than having imbricated loops.
Code sample to help
app/Controller/ClientsController.php
<?php
App::uses( 'AppController', 'Controller' );
class ClientController extends AppController {
public function portfolio( $clientId ) {
$this->Client->id = $clientId;
if( !$this->Client->exists() ) {
throw new NotFoundException( 'There are no client with this id' );
}
$data = $this->Client->portfolio( $clientId );
$this->set( compact( 'data' ) );
}
}
app/Model/Client.php
<?php
App::uses( 'AppModel', 'Model' );
class Client extends AppModel {
public $hasMany = array(
'Account' => array(
'className' => 'Account',
'foreignKey' => 'client_id'
)
);
public function portfolio( $clientId ) {
$holdings = $this->Account->Holding->find( 'all', $findParameters );
$foo = $this->Account->Holding->doThings( $holdings );
return $foo;
}
}
app/Model/Account.php
<?php
App::uses( 'AppModel', 'Model' );
class Account extends AppModel {
public $hasMany = array(
'Holding' => array(
'className' => 'Holding',
'foreignKey' => 'account_id'
)
);
public $belongsTo = array(
'Client' => array(
'className' => 'Client',
'foreignKey' => 'client_id'
)
);
}
app/Model/Holding.php
<?php
App::uses( 'AppModel', 'Model' );
class Holding extends AppModel {
public $belongsTo = array(
'Account' => array(
'className' => 'Account',
'foreignKey' => 'account_id'
)
);
public function doThings( $holdings ) {
foreach( $holdings as $key => $value ) {
// code...
}
return $bar;
}
}
Related
I'm developing an app using Laravel and Livewire, in which I have several post types, each one is store in the database using different views, because they differ in some fields, but in general are very similars. So I use one table to store them in the database.
The point is that I have one component (app/Http/Livewire/MyComponent.php & resources/views/livewire/my-component.blade.php) for each Post Type. In each one I have a method to store the data, but I know this is not the best way to do that.
I’ve been trying with Service Providers and Controllers, but not get the right way to do it.
The question is: What is the best way/practice to do this? Have a single class to store the data in the database, and call it from every where I need it.
Thanks you all.
Edit:
I have a class App\Http\Livewire\Blog to store Blog post type. Every thing works ok, but I want to put the method store() in a single class, and then call it from every Livewire's component class, because I store others post types in the same table in the database and I'm repeating the same code in every class for every component.
This is a simple example of my code:
<?php
namespace App\Http\Livewire\Blog;
use Livewire\Component;
use App\Models\BlogPosts;
use Illuminate\Support\Facades\Auth;
class Post extends Component
{
public $title, $content;
public function render()
{
return view('livewire.blog.post');
}
public function store()
{
$data = [
'title' => $this->title,
'content' => $this->content,
'author' => Auth::user()->id,
'type' => 'Blog Post',
// Other data ...
];
BlogPosts::create($data);
}
}
Can I do this in a Controller:
$this->User->read(null, $id);
$this->User->find('list');
Is it correct?
Am I using MVC correctly?
Can these easy functions be used in a Controller? Or, do I need to create these functions in the Model? Like Model->getUser(), and have that function use Model->read().
I know that functions it's called by Model, but, when I want pass some parameters, and function makes big, for example:
$this->User->find('all', array(
'conditions' => array(
'User.active' => true,
'User.group_id' => 3,
'User.age >=' => 18
)
));
Can I call this function in Controller, or need create a custom function in Model, to call it? Like... $this->User->findSomeCustomFunction($param1, $param2, $param3)?
TLDR:
It's "ok" to call a find() from your Controller, however best practice is to put any/all find()s in your models.
If you make a habit of putting all your find()s in your models, it will make it much easier to maintain your code in the long run.
Explanation/example:
In this case, as an example, you could start with a seemingly simple function:
//User model
public function getUsers() {
return $this->find('list');
}
But later, maybe you need something more along the lines of:
//User model
public function getUsers($opts = array()) {
$defaults = array(
'findType' => 'all',
'activeOnly' => true,
);
$params = array_merge($defaults, $opts);
$qOpts = array('conditions' => array());
//active only
if(!empty($params['activeOnly'])) $conditions[$this->alias.'.active'] = 1;
return $this->find($params['findType'], $qOpts);
}
(Pardon if there are many ways to make that code better - it was just off the top of my head - It gives you the idea.)
Keeping all your find()s in the Model also keeps you from having to search through each Controller every time you want to write a find() to determine if you've used a similar find() anywhere else. If you're programming as a team, that can be a nightmare, and you're almost guaranteed to be duplicating code.
It is perfectly fine to call Model->find() from a Controller. However, you will also want follow the DRY (Don't Repeat Yourself) principles. That basically means "Don't copy-paste code everywhere."
So, if you find that you need to make this exact Model->find() call from many Controller actions, it is considered good practice to abstract it into a function call against the Model. So yes, your Controllers would then call $this->User->findSomeCustomFunction().
I'm looking to use Lithium framework to build my application config interface as I like its minimal approach and the document-store (i.e. Mongodb) centric model.
However, (and I know its not quite released yet), there is little-to-no information, tutorials or examples out there to move you on from the simple blog tutorial.
What I am trying to do now is build an app that will show me the collections I have in Mongodb, and then let me work with which ever collection I choose. I can't seem to figure out:
a) how would I build a model that enumerates the collections - preferably according to my internal naming scheme,
b) how do I break the convention model so I can specify the name of the collection to use?
I think there are two things i'm struggling with to answer these two questions - perhaps a fundamental misunderstanding of how to move a model in MVC beyond the simple collection-model-controller-view examples, and secondly, the actual process of telling the mongo datasource what collection to use.
any pointers or examples, gratefully received.
Chris
update::
So I figured out how to set the collection - for reference you can set source in the $_meta array like this:
protected $_meta = array(
'source' => '<<collectionName>>'
);
still no idea how to use a Model that will list me all the collections I have in my DB though. Any ideas how to do that from a philosophical and also technological manner?
further update::
so I have got a bit further thanks to the comments below. At least I might now be able to re-phrase the question a bit. I can define my model something like this:
<?php
namespace app\models;
use lithium\data\Model;
class Posts extends \lithium\data\Model{
protected $_meta = array('source' => false);
public function testcolls(){
return (self::connection()->sources());
}
}
?>
then in my view I can use:
<?php foreach ($post->testcolls() as $coll): ?>
<h2><?=$coll ?></h2>
<?php endforeach; ?>
that works - however, what I really want to do is not create a 'testcolls' method in my Model but as Medhi suggested below, I need to override the find method. I can't seem to figure out how to do that and what it would need to return. The docs are not too clear on this.
final update
based on the comment below and a bit of experimentation, I came up with the following that works for being able to call find with a collection as a parameter.
model:
class Dataqueues extends \lithium\data\Model{
protected $_meta = array('source' => false);
public static function find($filter, array $options = array()) {
if (isset($options['collection'])){
self::meta('source', $options['collection']);
}
return parent::find('all',$options);
}
}
controller:
class DataqueuesController extends \lithium\action\Controller {
public function index() {
$dataqueues = Dataqueues::find('all',array('limit'=>20,'collection'=>'W501'));
return compact('dataqueues');
}
}
getting a model that returns a list of collections was also pretty simple in the end:
class Collections extends \lithium\data\Model{
protected $_meta = array('source' => false);
public static function find($filter, array $options = array()) {
return self::connection()->sources();
}
}
note that the controller won't support options or filters.
Nothing holds you from having a Collections Model, where you set $_meta['source'] = false to prevent Lithium from looking for a Collection in your database named collections.
In this model, you can call YourModel::connection()->sources() to list all your Mongo Collections.
Docs for sources(): http://li3.me/docs/lithium/data/source/MongoDb::sources(). Basically it calls listCollections() on a MongoDB instance http://php.net/manual/en/mongodb.listcollections.php
You can override your Model::find() method to return the list of collections, instead the list of documents, or pass the collection as a param Collections::find('all', array('conditions' => ..., 'collection' => 'foo'))... or wathever you want :-)
Lithium is designed to don't force that much on you !
First of all, Lithium follows the convention over configuration approach.
What this means:
Configuration: 'source' => '<< collectionName >>'
Convention: Name your model and your collection the same thing, the framework handles the rest.
IE: A "People" collection will have a "People" model
Second, connect to your database:
Configure your connections.php file in app\bootstrap\connections.php. I know I said convention over configuration, but you still need to let the framework know where the database is and what the login info is. For details look at the http://li3.me/docs/manual/quickstart. Here is the relevant code:
// MongoDB Connection
Connections::add('default', array('type' => 'MongoDb', 'database' => 'blog', 'host' => 'localhost'));
Third, get data
Create a model, matching your collection name, and then in your controller, add this line:
$model = Models::all();
where model is the singular name for what you are storing in your collection, and Models is the name of your model. That is it.
If you put a break point after this line, you will see your Models collection. For more information, see http://li3.me/docs/manual/working-with-data/using-models.wiki
Finally, to pass it to your view, simply put this line of code at the end of your controller:
return compact('model', 'model2', 'model3');
where model would be what you just pulled in the third step. The models 2 and 3 that I tacked on is how you would pass any other collections you pulled.
In your view, you would just reference $model to and assume that the relevant fields are there. You don't have to worry about putting getters or setters or anything else like that.
For example: if you want to show the data in $model:
foreach ($model as $aModel) {
echo $aModel;
}
See Accessing View Variables in: http://li3.me/docs/manual/handling-http-requests/views.wiki
Hope this helps.
Nobody seems to have a problem with that so either I'm doing it wrong or no one ever tried:
I have a model "Infocenter" which has many "InfocenterArticle"s. To fetch data including the related stuff I attached the Containable behavior to both.
This worked well until now that I attached a "HasImageAttachment" behavior implemented by myself. The problem is that on contained models the callbacks of my behavior don't get called.
My Models:
class Infocenter extends AppModel {
...
$actsAs = array('HasImageAttachment', 'Containable');
$hasMany = array('InfocenterArticle');
...
}
class InfocenterArticle extends AppModel {
...
$actsAs = array('Containable');
$belongsTo = array('Infocenter');
...
}
In my Controller I call:
$conditions = array('InfocenterArticle.id' => $id);
if ($this->notLoggedIn()) $conditions['InfocenterArticle.freigabe'] = 1;
$article = $this->InfocenterArticle->find('first', array(
'contain' => array(
'Infocenter',
'Infocenter.InfocenterArticle' => array(
'fields' => array('id', 'title', 'freigabe'),
'order' => array(
'InfocenterArticle.order_index' => 'desc',
'InfocenterArticle.created' => 'desc',
'InfocenterArticle.title' => 'asc'
),
'conditions' => array(
'InfocenterArticle.infocenter_id' => 'Infocenter.id'
),
),
),
'conditions' => $conditions,
));
And I can see that my HasImageAttachmentBehavior::setup() method is called but the HasImageAttachmentBehavior::afterFind() (as well as beforeFind()) are not. Infocenter::afterFind() is called though, which enabled me to do some dirty hacking, good enough for now, but I hate it.
What am I doing wrong?
Edit: Additional info in reply to RichardAtHome's comment.
1) My behavior works on models that don't have Containable attached.
2) I made sure that afterFind() doesn't get called by putting a simple die(); in the first line. The script doesn't die().
3) Signature should be okay, I double checked.
4) I'm using CakePHP 1.3.
Thanks for your help.
Currently I don't believe CakePHP core supports Behavior across Contained models.
It may be due to possible recursion, if you have weird contain array the behaviors may be called incorrectly.
There is a long post on the CakePHP Lighthouse project regarding calling behaviors over associated models, with a few recommendations for workarounds.
http://cakephp.lighthouseapp.com/projects/42648/tickets/95-afterfind-and-beforefind-callbacks-not-working-on-associated-models-was-translate-behavior-should-translate-associated-model-data
I just wrote an extensive entry on how to deal with this type of scenario.
It is for CakePHP 2.x and PHP 5.4+.
Containable behavior alternative function
Apparently this was a deliberate point of design(??!?). So you have to upgrade all the way to 3.0 if you want associated models to behave fully like models. Sigh.
Here is an extensive discussion: https://github.com/cakephp/cakephp/issues/1730
and the most straightforward cookbook fix: https://schneimi.wordpress.com/2009/09/06/behavior-afterfind-on-model-associations/
I'm new to CakePHP and I'm stuck in reading a Model using other fields. I did a cake bake command to generate a simple users CRUD. I can view the user using the url CakePHP provided.
/users/view/1
I can view the user using id = 1. What if I want to view a user by name instead of id?
/users/view/username
By default the view function reads the User model by id.
$this->User->read(null, $id)
Thank you.
you can use find function or findBy<Field>() in your case findByUsername()
check this
I've never used cakePHP myself but I'm going to suggest that you will likely have to implement a new user model method, something like getUserByUsername($username)
This would then in turn interface with your DAL that would get the details of that user based on the username and return a user object that can be used however you wish...
It seems that CakePHP is focusing to deprecate some functions, such as findAll(). Perhaps soon the magic methods such as findBy<field>() will have the same fate.
I can recommend what martswite is suggesting, you should create your custom function:
function findUser($username=''){
return $this->find('first', array(
'conditions' => array(
'User.username' => $username
)
));
}
Perhaps you have a status field, maybe the profile isn't public, you can add a condition:
function findUser($username=''){
return $this->find('first', array(
'conditions' => array(
'User.username' => $username,
'User.status' => 1
)
));
}
I think that's more modular than findBy<Field>.