I'm banging my head to find a solution to this but still i'm unable, I'm looking for a design vice solution not a hack to fix the issue.
I have following classes
class CourseService{
public function getCourse($courceId){
$course = $this->courseRepo->getCourse($courseId);
$restrictions = $this->invoiceService->getRestrictions($course->courseid);
$course->restrictions = [];
if($restrictions != null){
$course->restrictions = $restrictions;
}
}
}
Now this course service is injected in the constructor of the StudentService because when students need to enroll to a cource i use this course service there.
also you can see that I have used CourseRepo to get Course object and then InvoiceService to say which fields are restricted to update, basically restrictions attributes gives an array of strings defining which fields are not allowed to edit and I expect UI developer will use it to disable those fields, and I had to inject InvoiceService because there are some processing to do to the raw db records that are fetched from the InvoiceRepo so invoice repo is encapsulated in the invoiceService
now lets look at the InvoiceService
Class InvoiceService{
public function getAmountToPay($courseid, $studentid){
//now I need to inject StduentService inorder to get student info which needed for the calculation
}
}
but I can't inject StudentService into here because StudentService -> CourceService -> InvoiceService
Options I see and the consequences
One option I see is to get rid of InvoiceService from the CourseService and use InvoiceService in the place where the getCourse() get called and then modify the result but the problem is, CourseService is used mainly in controllers and next thing is that getCourse() get called from many controllers and service and expects the restrictions to be there so if I want to get rid of the InvoiceService then I'll have many places to add the removing lines and it crates a code repetition.
I can move getAmountToPay() to student service but then that service has already doing many student related tasks and i'm happy to extract just the invoice part to another service so I have a clear place to look when I need to check for bugs on invoices.
Student service:
First of all you have to see - actually to decide - that a student service uses an invoice service, not the reciprocal. When I enroll myself as a history student, I go to the registration/students office first. They are calling the financial/invoice office to ask about how much should I pay. The financial office checks in the database and returns the response regarding the amount to be payed by me.
Course service:
...The time passed by. Now I'm a student. I don't need to go to the registration office anymore. If I have to know something about my courses I go to the secretariat/course service. They'll give me all the informations I need about my courses. But, if I want to visit some special archeology course, where one must pay something, the course service will call the financial/invoice service to ask about that for me. They, in turn, will return the infos. The same applies if the course service wants to know about some financial restrictions I should have: they call the financial/invoice service.
Invoice service - student service, invoice service - course service:
Now, what should happen, if the invoice service needs infos about a student or a course? Should it call the student service, or the course service for that? The answer is no. The invoice service should receive a student id, a course id, a domain object Student, or a domain object Course as constructor/methods dependencies, but not the corresponding service(s). And it will fetch the infos it needs by itself. More of it, the invoice service should work with its specific invoice/financial tables, not with the course tables or the student details tables (except their id's).
Conclusions:
To enroll a student is the job of the StudentService. Though the
CourseService can assist the enrollment process.
StudentService verifies the amount to be paid by a student by calling
the InvoiceService. I know you don't want to have getAmountToPay()
inside the StudentService, but it's a natural workflow. You may think
of separate the other many things, for which the StudentService is
responsible, to another services.
The CourseService is responsible for finding a course, together with
the course restrictions, for which it calls the InvoiceService. So,
the CourseService will be assisted by the InvoiceService.
Down under I passed you the PHP version of my vision. I renamed some functions, to give you a better perspective.
Good luck!
P.S: I hope I understood right, that the sense of "invoice sevice" is a "financial department" one. Sorry, but I'm not a native english speaker, so I can't know all the senses.
<?php
class StudentService {
protected $courseService;
protected $invoiceService;
/**
* Even if the course service uses the invoice service,
* doesn't mean that the student service shouldn't use it too.
*
* #param CourseService $courseService
* #param InvoiceService $invoiceService
*/
public function __construct(CourseService $courseService, InvoiceService $invoiceService) {
$this->courseService = $courseService;
$this->invoiceService = $invoiceService;
}
/**
* Enroll a student to a course.
*
* #param integer $studentId
* #param integer $courseId
* #return bool Enrolled or not.
*/
public function enrollToCourse($studentId, $courseId) {
//... Use here the CourseService too - for what you said regarding the enrollment.
$enrolled = $this->studentRepo->enrollToCourse($studentId, $courseId);
return $enrolled;
}
/**
* Get the amount to be payed by a student on the enrollment moment.
*
* #param integer $studentId
* #param integer $courseid
* #return integer Amount to be payed.
*/
public function getAmountToPayOnEnrollment($studentId, $courseid) {
$amount = $this->invoiceService->getAmountToPayOnEnrollment($studentId, $courseid);
return $amount;
}
}
class CourseService {
protected $invoiceService;
/**
* Invoice service is used to get the (financial) restrictions for a course.
*
* #param InvoiceService $invoiceService
*/
public function __construct(InvoiceService $invoiceService) {
$this->invoiceService = $invoiceService;
}
/**
* Get a course and its corresponding (financial) restrictions list.
*
* #param integer $courseId
* #return Course Course domain object.
*/
public function getCourse($courseId) {
$course = $this->courseRepo->getCourse($courseId);
$course->restrictions = $this->getRestrictionsForCourse($course->courseId);
return $course;
}
/**
* Get the (financial) restrictions for a specified course.
*
* #param integer $courseId
* #return array Restrictions list.
*/
public function getRestrictionsForCourse($courseId) {
$restrictions = $this->invoiceService->getRestrictionsForCourse($courseId);
return $restrictions;
}
}
Class InvoiceService {
/**
* No student service needed!
*/
public function __construct() {
//...
}
/**
* Again, no student service needed: the invoice service
* fetches by itself the needed infos from the database.
*
* Get the amount to be payed by a student on the enrollment moment.
*
* #param integer $studentId
* #param integer $courseid
* #return integer Amount to be payed.
*/
public function getAmountToPayOnEnrollment($studentId, $courseid) {
$amount = $this->invoiceRepo->getAmountToPayOnEnrollment($studentId, $courseid);
return $amount;
}
/**
* Get the (financial) restrictions for a course.
*
* #param integer $studentId
* #param integer $courseid
* #return array Restrictions list.
*/
public function getRestrictionsForCourse($courseid) {
$restrictions = $this->invoiceRepo->getRestrictionsForCourse($courseid);
return isset($restrictions) ? $restrictions : [];
}
/*
* Quote: "Some processing to do to the raw
* db records that are fetched from the InvoiceRepo".
*/
//...
}
I would change your invoiceService to not depend on student service. Pass in what you need to the invoiceService. The logic of what to do with those student details can stay in invoiceService, but the content can be passed in.
Related
I've started creating a RESTful API (well, I did my best, I'm trying to follow the patterns) and I have stumbled upon a scenario that I'm not really sure how to handle. I will explain the current structure:
My application has 4 controllers:
Customers
Payments
Log
Taking as example the Customers controller, I have defined the following actions:
GET /customers: returns a list of customers
POST /customers: creates a new customer
GET /customers/{id}: returns the customer with the provided id
PUT /customers/{id}: updates the customer with the provided id
DELETE /customers/{id}: destroys the customer
This is the full code of the Customer controller:
namespace App\Http\Controllers;
use App\Customer;
use Illuminate\Http\Request;
class CustomerController extends Controller
{
/**
* Display a listing of the resource.
*
* #return \Illuminate\Http\Response
*/
public function index()
{
return Customer::all();
}
/**
* Store a newly created resource in storage.
*
* #param \Illuminate\Http\Request $request
* #return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$customer = Customer::create($request->all());
return response()->json($customer, 201);
}
/**
* Display the specified resource.
*
* #param \App\Customer $customer
* #return \Illuminate\Http\Response
*/
public function show(Customer $customer)
{
return $customer;
}
/**
* Update the specified resource in storage.
*
* #param \Illuminate\Http\Request $request
* #param \App\Customer $customer
* #return \Illuminate\Http\Response
*/
public function update(Request $request, Customer $customer)
{
$customer->update($request->all());
return response()->json($customer, 200);
}
/**
* Remove the specified resource from storage.
*
* #param \App\Customer $customer
* #return \Illuminate\Http\Response
*/
public function destroy(Customer $customer)
{
$customer->delete();
return response()->json(null, 204);
}
}
The code is very similar in the other controllers. It's also important to note that:
A Customer can have multiple Payments
A Customer can have multiple records in the Log
The problem starts here:
I need to display in the front-end a summary page with all customer data (name, email, registration date, etc) and a box showing the number of payments made and another box showing the number of entries in the Log.
Do I need to make 3 requests? (One to /customers/id, other to customers/id/payments and other to customers/id/logs)
If I return all the customer related data in the customers/id call, am I breaking the RESTful convention?
I am using apigility, but my answer still will be related to your question. According to the REST terminology (which could be find here https://apigility.org/documentation/intro/first-rest-service#terminology ) You are talking about entity and collection.
/customers/id - entity,
/customers/id/payments - collection,
/customers/id/logs - collection.
These are 3 different requests. So, yes, you need make 3 different requests.
But, to be honest, if you don't need pagination over payments and logs you can have only one request to /customers/id and within response you can have fields with array
{
"_links": {
"self": {
"href": "http://localhost:8080/status/3c10c391-f56c-4d04-a889-bd1bd8f746f0"
}
},
"id": "3c10c391-f56c-4d04-a889-bd1bd8f746f0",
...
_payments: [
...
],
_logs: [
...
],
}
Upd (duplicate from comment for future visitors).
Also, you should pay attention to DTO. I suppose this link will be interesting https://stackoverflow.com/a/36175349/1581741 .
Upd2.
At current moment I treat your collection /customers/id/payments like this:
/payments?user_id=123
where user_id is filtering field on payments table.
I think your problem that you confuse your REST API with your database. They don't have to follow the same structure. You can easily return the whole nested JSON for GET /customers/{id} if that's what you need from your REST API.
Based on a private messaging app, my user entity currently have two methods to retrieve messages :
One to get the messages sent by the user
/**
* #ORM\OneToMany(targetEntity="App\Entity\MessagePrive", mappedBy="emetteur")
*/
private $messagesPrivesEmis;
/**
* #return Collection|MessagePrive[]
*/
public function getMessagesPrivesEmis(): Collection {
return $this->messagesPrivesEmis;
}
and another one to get the messages received from other users
/**
* #ORM\OneToMany(targetEntity="App\Entity\MessagePrive", mappedBy="recepteur")
*/
private $messagesPrivesRecus;
/**
* #return Collection|MessagePrive[]
*/
public function getMessagesPrivesRecus(): Collection {
return $this->messagesPrivesRecus;
}
The first method get the messages where emetteur is equal to user id, while the second get the messages where recepteur is equal to user id. Both are Symfony default methods
Is it possible to "merge" those two methods so it get all messages sent and received by the user in one single query?
Or should I resort to custom DQL?
public function getMerged(): Collection {
return new ArrayCollection(
array_merge(this->messagesPrivesEmis->toArray(), $this->messagesPrivesRecus->toArray())
);
}
Sorry if this is a stupid question, but I'm new to Laravel.
I have two models and a pivot table:
User
id | name | password
public function conversations(): ?BelongsToMany
{
return $this->belongsToMany(Conversation::class)->withTimestamps();
}
Conversation
id
public function users(): ?BelongsToMany
{
return $this->belongsToMany(User::class)->withTimestamps();
}
conversation_user
id | conversation_id | user_id
I create a conversation and assign the users with sync like so:
$user->conversations()->syncWithoutDetaching($conversation);
$targetUser->conversations()->syncWithoutDetaching($conversation);
Users can have many conversations, and conversations can have multiple users. This is fine, but when I want to get a conversation with two specific users I don't know the best way to utilize the ORM to find the conversation they're both apart of.
I am currently using this next method, which works but it feels like there is a much better way of doing things utilizing the ORM:
/**
* Get a conversation by a target user id.
*
* #param int $targetUserId
* #return mixed
*/
public function getConversationByTargetUserId(int $targetUserId)
{
// Get the current user.
$user = Auth::guard()->user();
// Check the user exists.
if (!$user) {
throw new HttpException(500);
}
/**
* Get all pivot tables where the
* user ID is from the current user.
*/
$userConversationIdsArray = DB::table('conversation_user')->where('user_id', $user->id)->pluck('conversation_id');
/**
* Get all pivot tables where the user
* id is equal to the target id, and is
* also owned by the current user. Return
* the first instance that we come across.
*/
$targetConversation = DB::table('conversation_user')->where(['conversation_id' => $userConversationIdsArray, 'user_id' => $targetUserId])->first();
/**
* Return the conversation.
*/
return Conversation::find($targetConversation->conversation_id);
}
Thank you for your time :)
Is there a particular reason you are not utilising Eloquent? It might make it easier.
It could be done like this as you already have the user.
$user->conversations()->has('users.id', '=', $targetUserId)->first();
(I have not tested this solution so i am not sure this works 100%)
Also, there might be a typo in your first query. Might be a copy paste error might be a typo. Just making sure.
$userConversationIdsArray = DB::table('conversation_user')->where('user_id', $user->id)->pluck('id'); <---- 'id' shouldn't that be 'conversation_id'?
Thanks to #Fjarlaegur they put me on the right track. The following method works:
/**
* Get a conversation by a target user id.
*
* #param int $targetUserId
* #return mixed
*/
public function getConversationByTargetUserId(int $targetUserId)
{
// Get the current user.
$user = Auth::guard()->user();
// Check the user exists.
if (!$user) {
throw new HttpException(500);
}
return $user->conversations()->whereHas('users', function ($query) use ($targetUserId) {
$query->where('users.id', $targetUserId);
})->first();
}
Does Laravel allow us to add multiple Policies for a Model? I.e. consider App\Providers\ASuthServiceProvider's $policies property:
protected $policies = [
'App\Team' => 'App\Policies\TeamPolicy',
'App\Team' => 'App\Policies\RoundPolicy',
'App\Team' => 'App\Policies\AnotherPolicy',
];
I haven't tested it in an application, because even if it worked, I would be here asking a similar question, regarding whether this is considered bad practise or prone to unexpected behaviour.
The alternative I have is a very messy Policy, containing policies relating to several controllers, named in camel case:
/**
* Allows coach of Team and admin to see the Team management view.
* Used in TeamManagementController
*
* #param App\User $user
* #param App\Team $team
* #return boolean
*/
public function manage(User $user, Team $team)
{
return $user->id === $team->user_id || $user->isAdmin();
}
/**
* Allows a coach to detach themself from a Team.
* Used in TeamController
*
* #param App\User $user
* #param App\Team $team
* #return boolean
*/
public function detach(User $user, Team $team)
{
return $user->id === $team->user_id;
}
/**
* Below function are used for controllers other than TeamController and TeamManagementController.
* Reason: We need to authorize, based on a Team. Hence, using this Policy.
*/
/**
* Allows coach of Team, as well as admin to view players of a Team.
* Used in PlayerController
*
* #param App\User $user
* #param App\Team $team
* #return boolean
*/
public function indexPlayers(User $user, Team $team)
{
return $user->id === $team->user_id || $user->isAdmin();
}
/**
* Allows coach of Team, as well as admin to view players of a Team as an array.
* Used in PlayerController
*
* #param App\User $user
* #param App\Team $team
* #return boolean
*/
public function fetchPlayers(User $user, Team $team)
{
return $user->id === $team->user_id || $user->isAdmin();
}
etc. etc.
You could use traits to separate the logic for your policy.
You would create a base TeamPolicy and then multiple traits with the various methods that you would want within the base class.
<?php
class TeamPolicy
{
use RoundPolicy, AnotherPolicy;
}
The $policies variable uses the model as key and as value a policy. Keys are unique so you can only set one policy per model. However you can use a policy on multiple models.
In your case the App\Policies\AnotherPolicy is the only one which will be used. Also assigning multiple models the same policy really depends on what you want to do. Basically you do not want messy or gross code. So if you create a policy for two models and the policy code becomes too large, it is time to consider if creating another policy would make the code simpler/less gross.
You need to create a model class that will be associated with the policy.
protected $policies = [
'App\Team' => 'App\Policies\TeamPolicy',
'App\Round' => 'App\Policies\RoundPolicy',
'App\Another' => 'App\Policies\AnotherPolicy',
];
Create model classes which extend the Team class. The advantage of this approach is to have separate relationships and functions for respective business logic.
namespace App\Models;
class Round extend Team
I'm writing some code that allows users to read reports on a site, using AJAX calls to dynamically load only what is requested, instead of the entire 15+MB report.
I'm writing a Model to access all the report data from the database, and I don't want to use the Active Record pattern. I'm following the idea of "A Model HAS a table, instead of IS-A table", since this model will be accessing 5 different tables, and there are some complex MySQL JOIN's between these tables.
What is a good design pattern to follow in Zend Framework for this, examples?
UPDATED on 2012-12-05 # 12:14PM EST
I'm currently working for a Market Research Report company. Without using actual function names, or revealing any meaningful details of the code, here are the basics:
readreportAction() does:
get the report meta data
get the report "table of contents"
readsectionAction() does:
get the report text, only a part of it
get the embedded tabular data
get the figures / images
get the footnotes
format the report text
reportpdfAction() does the exact same thing as readreportAction() and readsectionAction(), except all at one time. I'm trying to conceptualize a way to NOT copy + paste this code / programming logic. A data mapper seems to solve this.
I would recommend the Data Mapper pattern.
Everything you said makes sense and this pattern fits. Your model should not know or care how it is persisted. Instead the mapper does what it suggests - maps your model to your database. One of the things I like about this approach is it encourages people to think about the model in terms of an object, not a relational database table, as often happens with active record patterns and table row gateways.
Your object, unless very simple, typically will not reflect the structure of a database table. This lets you write good objects and then worry about the persistence aspects afterward. Sometimes more manual in that your mapper will need to deal with the complex joins, probably requiring writing some code or SQL, but the end result is it does just what you want and nothing more. No magic or conventions required if you don't want to leverage them.
I've always though these articles do a good job of explaining some of the design patterns that can be used well in ZF: http://survivethedeepend.com/zendframeworkbook/en/1.0/implementing.the.domain.model.entries.and.authors#zfbook.implementing.the.domain.model.entries.and.authors.exploring.the.entry.data.mapper
UPDATE:
Well you mapper might extend from an interface similar to this:
<?php
interface Mapper_Interface
{
/**
* Sets the name of the entity object used by the mapper.
*/
public function setObjectClass($class);
/**
* Sets the name of the list class used by the mapper.
*/
public function setObjectListClass($listClass);
/**
* Get the name of the object class used by the mapper.
*
*/
public function getObjectClass();
/**
* Get the name of the object list class used by the mapper.
*
* #return string
*/
public function getObjectListClass();
/**
* Fetch one row.
*
* #param array $where Criteria for the selection.
* #param array [$order = array()] Optionally the order of results
* #return Object_Abstract
* #throws Mapper_Exception
*/
public function fetchRow($where, $order = array());
/**
* Fetch all records. If there is no underlying change in the persisted data this should
* return a consistant result.
*
* #param string|array|Zend_Db_Table_Select $where OPTIONAL An SQL WHERE clause or Zend_Db_Table_Select object.
* #param string|array $order OPTIONAL An SQL ORDER clause.
* #param int $count OPTIONAL An SQL LIMIT count.
* #param int $offset OPTIONAL An SQL LIMIT offset.
* #return Object_List_Abstract
* #throws Mapper_Exception
*/
public function fetchAll($where = null, $order = null, $count = null, $offset = null);
/**
* Deletes one or more object.
*
* #param array|string $where Criteria for row deletion.
* #return integer $affectedRows
* #throws Mapper_Exception
*/
public function delete($where);
/**
* Saves a record. Either updates or inserts, as required.
*
* #param $object Object_Abstract
* #return integer $lastInsertId
* #throws Mapper_Exception
*/
public function save($object);
}
And you would interact with the mapper like:
$fooObjectMapper = new Foo_Mapper;
$fooObjectList = $fooObjectMapper->fetchAll();
var_dump($fooObjectList->first());
or
$fooObjectMapper = new Foo_Mapper;
$fooObject = $fooObject->fetch(array('id = ?' => 1));
$fooObject->setActive(false);
$fooObjectMapper->save($fooObject);
I usually write a mapper abstract for any 'PDO' enabled databases. One of the attributes of that concrete mapper is then the Zend_Db_Adapter to issue commands against. Makes for a flexible solution, easy to use mock data sources in testing.
First it looks like you need to make a little bit more of a conceptual leap. With the data mapper pattern it helps to think in terms of objects instead of database tables. I found these two articles helpful when I needed to make the leap.
http://phpmaster.com/building-a-domain-model/
http://phpmaster.com/integrating-the-data-mappers/
That being said ZF 1 has some very useful tools for building a data mapper/domain model.
The convention in ZF 1 is for each table you are working with to be accessible through the Zend_Db_Table api. The simplest way I've found is to just use the DbTable resource for each table. You could also use the Zend_Db::factory or new Zend_Db_Table('tableName') or any other method that appeals to you.
This example is based on a mp3 song track.
//in effect this is the database adapter for database table 'track', This is $tableGateway used later.
<?php
class Application_Model_DbTable_Track extends Zend_Db_Table_Abstract
{
//name of database table, required to be set if name of class does not match name of table
protected $_name = 'track';
//optional, column name of primary key
protected $_primary = 'id';
}
there are several ways to attach a table to the Db adapter and the Zend_Db_Table api, I just find this method simple to implement and it makes setting up a mapper simple as well.
The mapper class is the bridge between the data source and your object (domain entity). The mapper interacts with the api for Zend_Db_Table in this example.
A really important point to understand: when using classes that extend Zend_Db_Table_Abstract you have all the basic functionality of the Zend_Db component at your disposal. (find(),fetchall(), fetchRow(), select() ...)
<?php
class Music_Model_Mapper_Track extends Model_Mapper_Abstract
{
//the mapper to access the songs artist object
protected $artistMapper;
//the mapper to access to songs album object
protected $albumMapper;
/**
* accepts instance of Zend_Db_Table_Abstract
*
* #param Zend_Db_Table_Abstract $tableGateway
*/
public function __construct(Zend_Db_Table_Abstract $tableGateway = null)
{
//at this point I tend to hardcode $tablegateway but I don't have to
$tableGateway = new Application_Model_DbTable_Track();
parent::__construct($tableGateway);
//parent sets the $tablegateway variable and provides an abstract requirement
//for createEntity(), which is the point of this class
}
/**
* Creates concrete object of Music_Model_Track
*
* #param object $row
* #return Music_Model_Track
*/
public function createEntity($row)
{
$data = array(
'id' => $row->id,
'filename' => $row->filename,
'format' => $row->format,
'genre' => $row->genre,
'hash' => $row->hash,
'path' => $row->path,
'playtime' => $row->playtime,
'title' => $row->title,
'track_number' => $row->track_number,
'album' => $row->album_id,//foriegn key
'artist' => $row->artist_id//foriegn key
);
//instantiate new entity object
return new Music_Model_Track($data);
}
/**
* findById() is proxy for find() method and returns
* an entity object.
*
* #param type $id
* #return object Model_Entity_Abstract
*/
public function findById($id)
{
//instantiate the Zend_Db_Select object
$select = $this->getGateway()->select();
$select->where('id = ?', $id);
//retrieve one database table row
$row = $this->getGateway()->fetchRow($select);
//create one entity object Music_Model_Track
$entity = $this->createEntity($row);
//return one entity object Music_Model_Track
return $entity;
}
//truncated
}
All that has gone before is for the express purpose of building the following object:
<?php
class Music_Model_Track extends Model_Entity_Abstract
{
/**
* $id, __set, __get and toArray() are implemented in the parent
*/
protected $album;
protected $artist;
protected $filename;
protected $format;
protected $genre;
protected $hash;
protected $path;
protected $playtime;
protected $title;
protected $track_number;
//artist and album mappers
protected $albumMapper = null;
protected $artistMapper = null;
//these are the important accessors/mutators because they convert a foreign key
//in the database table to an entity object.
public function getAlbum()
{
//if the album object is already set, use it.
if(!is_null($this->album) && $this->album instanceof Music_Model_Album) {
return $this->album;
} else {
//else we make a new album object
if(!$this->albumMapper) {
$this->albumMapper = new Music_Model_Mapper_Album();
}
//This is the album object we get from the id in our reference array.
return $this->albumMapper->findById($this->getReferenceId('album'));
}
}
//same as above only with the artist object.
public function getArtist()
{
if(!is_null($this->artist) && $this->artist instanceof Music_Model_Artist) {
return $this->artist;
} else {
if(!$this->artistMapper) {
$this->artistMapper = new Music_Model_Mapper_Artist();
}
return $this->artistMapper->findById($this->getReferenceId('artist'));
}
}
//the setters record the foriegn keys recorded in the table row to an array,
//this allows the album and artist objects to be loaded only when needed.
public function setAlbum($album)
{
$this->setReferenceId('album', $album);
return $this;
}
public function setArtist($artist)
{
$this->setReferenceId('artist', $artist);
return $this;
}
//standard setter and getters truncated...
}
so when using the track object you would get album or artist info like:
//this would be used in a controller most likely.
$mapper = new Music_Model_Mapper_Track();
$track = $mapper->findById('1');
//all of the information contained in the album or artist object is
//available to the track object.
//echo album title, year or artist. This album object also contains the artist object
//so the artist object would be available in two ways.
echo $track->album->title; //or
echo $track->artist->name;
echo $track->album->artist->name;
echo $track->getAlbum()->getArtist()->getName();
So what you really need to decide is how you want to structure your application. What I see as obvious may not be an option you wish to implement. A lot of the answers to your questions depend on exactly how these resources are to be used.
I hope this helps you at least a little bit.
You could consider using Doctrine 2. It's an ORM that does not use the ActiveRecord pattern.
In Doctrine, your models (entities) are all just normal PHP objects with zero knowledge of the database. You use mapping (xml, yaml or annotations) to tell Doctrine how they appear in the database, and the Entity Manager and repositories are used as a gateway for persisting entities or doing other database actions.