I have a very cohesive relation between Order
and Item
models.
Order hasMany Item
Item belongsTo Order
Item hasMany ChildItem
ChildItem is alias for Item (it's a recursive model)
The Order Model has a special Order::prepare()
function. It fires a Order.prepare
event on all attached Behaviors, which control, validate and modify Item data, such as shipping, item/order weights, quantity, discounts, ie. validating and staging the data for a save operation. It also sets the Order.total, Order.weight, Order.status, ...
fields.
Any behavior can also stop the preparation based on its constraints (stock limits, weight limits, vat, anything).
When Item
is added to an existing Order
:
Item.subtotal
s, Order.total
, weights and such are recalculated ...)After carefully considering multiple approaches I chose the third, but I got stuck:
beforeSave callbacks
This callback would ensure that these callbacks and calculations run on every item, but it makes it harder to retrieve related order data and items and run callbacks. It is also harder to know for sure if the data has already been prepared or not and recursion is also a problem. Harder to return prepared data instead of saving it.
Extending the Model::save
methods
Recursion is also a problem, no way of returning the prepared data without saving and I generally avoid extending save().
Decoupling the preparation and save processes Creating a special workflow for order data manipulation through use of custom Model methods and callbacks (eg. Order::prepare and Order::commit). Behaviors run on non-standard events (beforePrepare, beforeCommit, etc..) and Model::save() stays untouched.
I would gladly provide additional details, but it is really a massive model and it would be a long question, I've summarized as much as possible. Any ideas or examples regarding the correct approach will be much appreciated.
I am not sure how complex your system is - but why dont you create an OrderPrepare class ( i typically make this class extend Object not AppModel or Order, and put it in the Model folder or I put it in the Lib folder), pass in the Order object and then you can perform whatever logic you want in that class?
I have managed to battle through this endeavor and I am posting the result and my current solution. I do not think it is completely conventional workflow, but it does the job quite well.
I have created BaseOrder
and BaseItem
classes, which extend AppModel and are meant to be extended with your own class.
I have created BaseOrder::fetch()
, BaseOrder::stage()
and BaseOrder::commit()
methods, which are the core operations intended for work. All of them trigger before and after callbacks, which are custom event names, posted with a custom OrderEvent (extends CakeEvent) object.
BaseOrder::fetch()
This is used to get the current state of the Order from the database.
Order hasMany Coupon
), modify query=>contain
data, to ensure Order::fetch() retrieves the proper data for the workflow.BaseOrder::stage()
Recieves any kind of Order-related data as an argument, does a Order::fetch()
, applies and modifies the data and runs all the behaviors' and OrderItem's beforeStage
, stage
and afterStage
callbacks.
This method is expected to return a real state of the order, with all details resolved, such as product data, financial details, discounts, Behavior-attached data, etc.
BaseOrder::commit()
Takes a staged order array, validates financials, commits the data to the database.
Additionally, the BaseItem
and BaseOrder
both have an afterSave
callback, which triggers a recalculation of the related Order record's financials, in case any data is saved manually to the database.
This way, I can easily stage/commit an Order for all the bells and whistles, or I can update the order/item data directly and still have the financial data integrity.
I am thinking of moving the calculation side of things to a MySQL stored procedure, which would run automatically after each save, but it takes the fine-tune control out of the app (like round-off errors, default tax rates, etc.)
I am expecting someone to go no, no, no, you're doing it wrong and enlighten me with a better idea since all of this feels quite dirty.
The biggest "issue" I have is getting all the related BaseItem-extending Models and traversing the data array to "fish" them out for calculation. This allows me to have different models (like ProductItem, ShippingItem) and different relations between them, but presents a slight overhead.