Agile toolkit CRUD - php

I'm testing Agile Toolkit and i don't understand what i'm doing bad.
I've a table named 'families' which CRUD works fine.
Another table named 'subfamilies' with a relationship n to 1 with families and CRUD works fine too.
But here is my problem, in table 'articles' i've a relationship 1 to 1 with families and another 1 to 1 with subfamilies and CRUD works, i can add delete and modify but when add button is clicked, if i select a family on the form, subfamilies combobox shows me all subfamilies not only ones that are from that family.
How can i specify that if a family is selected the combobox from the add form only show subfamilies relatives to that family?
Code:
file ./lib/Model/Articulos.php:
class Model_Articulos extends Model_Table {
public $entity_code = 'articulos';
function init(){
parent::init();
$this->addField('name')->mandatory(true);
$this->addField('description')->mandatory(true)->type('text');
$this->addField('familias_id')->mandatory(true)->refModel('Model_Familias');
$this->addField('subfamilias_id')->refModel('Model_Subfamilias');
}
}
file ./page/articulos.php:
class page_articulos extends Page {
function init(){
parent::init();
$crud=$this->add('CRUD');
$crud->setModel('Articulos');
if($crud->grid){
$crud->grid->getColumn('name');
$crud->grid->getColumn('familias');
$crud->grid->getColumn('subfamilias');
}
}
}
And finally you can get the mysql workbench eer model here
Thank you in advance,
Serxoz.

Technically, when you define a model, there is a no sub-link between family and subfamily, so your CRUD wouldn't know about the dependency.
To do what you need, you must fully understand the following two examples:
http://demo.atk4.com/demo.html?t=20
http://demo.atk4.com/demo.html?t=22
Next, create a form for adding a new record for Articulous. You will need to use MVCForm but will have to add some additional code for the sub-family field. When you have it done, you'll need to convert your code into a separate class such as Form_Articulos inherited from MVCForm. Your code should go inside the "setModel" method of your form.
Next, extend CRUD like this:
class MyCrud extends CRUD {
public $form_class='Form_Articulos';
}
The new form will be used for editing and addition. One remaining thing you may need is to add model-level validation.
class Model_Articulous ... {
...
function beforeUpdate(&$data){
$family_id=$this->getRef('subfamilias_id')->get('familia_id');
if($family_id != $this->get('familias_id'))
throw $this->exception('Subfamily does not belong to selected family');
}
}

Related

Retrieve Parent Model Through Pivot Table Laravel

I'm currently struggling with retrieving data towards a parent model. I'll drop my database, classes, and things I've tried before.
I have 4 tables: sales_orders, products, work_orders, and product_sales_order (pivot table between sales_orders and products).
SalesOrder.php
class SalesOrder extends Model
{
public function products()
{
return $this->belongsToMany(Product::class)
->using(ProductSalesOrder::class)
->withPivot(['qty', 'price']);
}
}
ProductSalesOrder.php
class ProductSalesOrder extends Pivot
{
public function work_orders()
{
return $this->hasMany(WorkOrder::class);
}
public function getSubTotalAttribute()
{
return $this->qty* $this->price;
}
}
WorkOrder.php
class WorkOrder extends Model
{
public function product_sales_order()
{
return $this->belongsTo(ProductSalesOrder::class);
}
public function sales_order()
{
return $this->hasManyThrough(
ProductSalesOrder::class,
SalesOrder::class
);
}
}
So, what I want to retrieve sales order data from work order since both tables don't have direct relationship and have to go through pivot table and that is product sales order. I've tried hasOneThrough and hasManyThrough but it cast an error unknown column. I understand that error and not possible to use that eloquent function.
Is it possible to retrieve that sales order data using eloquent function from WorkOrder.php ?
You cannot achieve what you want using hasOneThrough as it goes from a table that has no ID related to the intermediate model.
In your example you are doing "the inverse" of hasOneThrough, as you are going from a model that has the ID of the intermediate model in itself, and the intermediate model has the ID of your final model. The documentation shows clearly that hasOneThrough is used exactly for the inverse.
So you still should be able to fix this, and use a normal relation as you have the sales_orders_id in your model SuratPerintahKerja, so you can use a normal relation like belongsTo to get just one SalesOrder and define it like this:
public function salesOrder()
{
return $this->belongsTo(SalesOrder::class, 'sale_orders_id');
}
If you want to get many SalesOrders (if that makes sense for your logic), then you should just run a simple query like:
public function salesOrders()
{
return $this->query()
->where('sale_orders_id', $this->sale_orders_id)
->get();
}
Have in mind that:
I have renamed your method from sales_order to salesOrder (follow camel case as that is the Laravel standard...).
I have renamed your method from sales_order to salesOrders for the second code as it will return more than 1, hence a collection, but the first one just works with one model at a time.
I see you use sale_orders_id, but it should be sales_order_id, have that in mind, because any relation will try to use sales_order_id instead of sale_orders_id, again, stick to the standards... (this is why the first code needs more parameters instead of just the model).
All pivot tables would still need to have id as primary and auto incremental, instead of having the id of each related model as primary... Because in SuratPerintahKerja you want to reference the pivot table ProdukSalesOrder but it has to use both produks_id (should have been produk_id singular) and sale_orders_id (should have been sales_order_id). So if you were able to use something like produk_sales_order_id, you could be able to have better references for relations.
You can see that I am using $this->query(), I am just doing this to only return a new query and not use anything it has as filters on itself. I you still want to use current filters (like where and stuff), remove ->query() and directly use the first where. If you also want to add ->where('produks_id', $this->produks_id) that is valid and doesn't matter the order. But if you do so, I am not sure if you would get just one result, so ->get() makes no sense, it should be ->first() and also the method's name should be salesOrder.
Sorry for this 6 tip/step, but super personal recommendation, always write code in English and do not write both languages at the same time like produks and sales orders, stick to one language, preferrably English as everyone will understand it out of the box. I had to translate some things so I can understand what is the purpose of each table.
If you have any questions or some of my code does not work, please tell me in the comments of this answer so I can help you work it out.
Edit:
After you have followed my steps and changed everything to English and modified the database, this is my new code:
First, edit ProductSalesOrder and add this method:
public function sales_order()
{
return $this->belongsTo(SalesOrder::class);
}
This will allow us to use relations of relations.
Then, have WorkOrder as my code:
public function sales_order()
{
return $this->query()->with('product_sales_order.sales_order')->first();
}
first should get you a ProductSalesOrder, but then you can access ->sales_order and that will be a model.
Remember that if any of this does not work, change all the names to camelCase instead of kebab_case.

Advanced Laravel merged data/models - can it be done at model level?

We have a COMMON database and then tenant databases for each organization that uses our application. We have base values in the COMMON database for some tables e.g.
COMMON.widgets. Then in the tenant databases, IF a table called modified_widgets exists and has values, they are merged with the COMMON.widgets table.
Right now we are doing this in controllers along the lines of:
public function index(Request $request)
{
$widgets = Widget::where('active', '1')->orderBy('name')->get();
if(Schema::connection('tenant')->hasTable('modified_widgets')) {
$modified = ModifiedWidget::where('active', '1')->get();
$merged = $widgets->merge($modified);
$merged = array_values(array_sort($merged, function ($value) {
return $value['name'];
}));
return $merged;
}
return $countries;
}
As you can see, we have model for each table and this works OK. We get the expected results for GET requests like this from controllers, but we'd like to merge at the Laravel MODEL level if possible. That way id's are linked to the correct tables and such when populating forms with these values. The merge means the same id can exist in BOTH tables. We ALWAYS want to act on the merged data if any exists. So it seems like model level is the place for this, but we'll try any suggestions that help meet the need. Hope that all makes sense.
Can anyone help with this or does anyone have any ideas to try? We've played with overriding model constructors and such, but haven't quite been able to figure this out yet. Any thoughts are appreciated and TIA!
If you put this functionality in Widget model you will get 2x times of queries. You need to think about Widget as an instance, what I am trying to say is that current approach does 2 queries minimum and +1 if tenant has modified_widgets table. Now imagine you do this inside a model, each Widget instance will pull in, in a best case scenario its equivalent from different database, so for bunch of Widgets you will do 1 (->all())+n (n = number of ModifiedWidgets) queries - because each Widget instance will pull its own mirror if it exists, no eager load is possible.
You can improve your code with following:
$widgets = Widget::where('active', '1')->orderBy('name')->get();
if(Schema::connection('tenant')->hasTable('modified_widgets')) {
$modified = ModifiedWidget::where('active', '1')->whereIn('id', $widgets->pluck('id'))->get(); // remove whereIn if thats not the case
return $widgets->merge($modified)->unique()->sortBy('name');
}
return $widgets;
OK, here is what we came up with.
We now use a single model and the table names MUST be the same in both databases (setTable does not seem to work even though in exists in the Database/Eloquent/Model base source code - that may be why it's not documented). Anyway = just use a regular model and make sure the tables are identical (or at least the fields you are using are):
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Widget extends Model
{
}
Then we have a generic 'merge controller' where the model and optional sort are passed in the request (we hard coded the 'where' and key here, but they could be made dynamic too). NOTE THIS WILL NOT WORK WITH STATIC METHODS THAT CREATE NEW INSTANCES such as $model::all() so you need to use $model->get() in that case:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class MergeController extends Controller
{
public function index(Request $request)
{
//TODO: add some validations to ensure model is provided
$model = app("App\\Models\\{$request['model']}");
$sort = $request['sort'] ? $request['sort'] : 'id';
$src_collection = $model->where('active', '1')->orderBy('name')->get();
// we setup the tenants connection elsewhere, but use it here
if(Schema::connection('tenant')->hasTable($model->getTable())) {
$model->setConnection('tenant');
$tenant_collection = $model->get()->where('active', '1');
$src_collection = $src_collection->keyBy('id')->merge($tenant_collection->keyBy('id'))->sortBy('name');
}
return $src_collection;
}
}
If you dd($src_collection); before returning it it, you will see the connection is correct for each row (depending on data in the tables). If you update a row:
$test = $src_collection->find(2); // this is a row from the tenant db in our data
$test->name = 'Test';
$test->save();
$test2 = $src_collection->find(1); // this is a row from the tenant db in our data
$test2->name = 'Test2'; // this is a row from the COMMON db in our data
$test2->save();
dd($src_collection);
You will see the correct data is updated no matter which table the row(s) came from.
This results in each tenant being able to optionally override and/or add to base table data without effecting the base table data itself or other tenants while minimizing data duplication thus easing maintenance (obviously the table data and population is managed elsewhere just like any other table). If the tenant has no overrides then the base table data is returned. The merge and custom collection stuff have minimal documentation, so this took some time to figure out. Hope this helps someone else some day!

How to make models use defaults in Phalcon PHP Framework?

If a table has defaults on certain fields and NULL is not allowed, one would expect the insert script to use those defaults, as MariaDB/MySQL usually does. For example, if the table products has an AI field "id", a required field "name" and two required fields "active" and "featured" which both default to 1, then the query
INSERT INTO products (name) VALUES ('someName');
automatically inserts 1 as the value of active and featured. However, when using Phalcon's models like so:
$product = new Products();
$product->setName('someName');
$product->save();
returns validation errors saying "active" and "featured" are required.
Is there a flag I should provide during model generation in order for Phalcon tools to harvest and input the defaults into Model classes, or another way to make Phalcon automatically use defaults if found? Best approach would be just ignoring the fields that weren't set, I reckon. Can I make the models do that?
You can use a raw database value to avoid that, in specific inserts:
<?php
use Phalcon\Db\RawValue;
$product = new Products();
$product->setName('someName');
$product->setType(new RawValue('default')); //use default here
$product->save();
Or, general before create/update for specific fields:
use Phalcon\Db\RawValue;
class Products extends Phalcon\Mvc\Model
{
public function beforeValidationOnCreate()
{
$this->type = new RawValue('default');
}
}
Or ignore these fields in every SQL INSERT generated:
use Phalcon\Db\RawValue;
class Products extends Phalcon\Mvc\Model
{
public function initialize()
{
$this->skipAttributesOnCreate(array('type'));
}
}
Although I find twistedxtra's answer fascinating from the aspect that Phalcon contains this wicked method to read the column default, I believe from a architectural point of view this might be the wrong approach as you rely on your database to define the defaults of the properties of your model.
I would set the default value when declaring the property and keep the logic in the application layer. But that's just me.
Use Like below
The skipAttributesOnCreate will make sure Phalcon does not attempt to put a a value in that column. The database will apply the default value.
public function initialize()
{
$this->setSource('table_name');
$this->skipAttributesOnCreate(['name_of_column']);
}

Data can't save after alter the table in yii

I new in yii framework. i create an application in yii framework. i created model, controller, views using gii. After that i alter database table. I deleted 2 column and add 3 new columns. After that overwrite the model using the gii. But when i am trying to save into that table it show property(which was old column that I deleted) is not defined. Plz provide me a solution for this.
You need to define all columns in the validation rules() method in your model, have a look and make sure that you have defined a rule for every column in the table there, for example (if it's a string with max length 128):
public function rules()
{
return array(
...
array('myField', 'length', 'max'=>128),
...
);
}
See some info about validation rules.
Also, for forms if you're using CActiveForm widget and calling fields like so:
echo $form->labelEx($model,'myField');
echo $form->textField($model,'myField');
Then you'll need to make sure that a label is defined in the model too, in the attributeLabels() method, for example:
public function attributeLabels()
{
return array(
...
'myField'=>'My Field',
...
);
}
Lastly, if you want the field to be searchable, you'll need to add a statement to the search() method in the model, for example:
public function search()
{
...
$criteria->compare('myField',$this->myField);
...
}
Make sure you have all of those elements present and you shouldn't get the '* is not defined' error.
Also, if you're using schema caching in your main config file, you'll have to clear your cache before the app will see your new database structure.
Your changes should also be set at the Views since there are forms, widgets using the old properties !! (for this exact save issue, you will need to fix _form.php which is the partial responsible from your model Save & Update actions.
You can either do the same as you did with the model: (regenerate it using gii) or you can edit it manually (i recommend you get used to this since in the future you will have code you don't want to loose just because of altering a column name. simple Find & edit in most of the text editors will do the job).
May be you need to read a bit more about how MVC works in general & in Yii in special
This is because you are using schema-cache. Your table schema is cached in Yii. You need to flush AR cache. Either flush full schema cache or use
Yii::app()->db->schema->getTable('tablename', true); in start of your action. This will update model schema-cache.

Multi Table Update/Delete in AtK4

How can one achieve multi table update/delete in atk4?
You can add related entities on the model level:
https://stackoverflow.com/a/7466839/204819
If this is not an option, you can always create custom handlers for your "edit" and "delete" button and perform actions using internal ORM or even on Model level.
You can use functions beforeInsert, afterInsert, beforeDelete, afterDelete and beforeUpdate, afterUpdate to carry out additional processing in the database. For example
using a unzipped install of ATK 4.1.3 in your webroot creates a folder called agiletoolkit which is referred to below as ATKHOME.
Create a simple table TASKTYPE in mysql with three fields (id, tasktype_desc and budget_code) and another table TASKTYPE_BUDGET with just id and budget_code.
Create two models for the tables in ATKHOME/lib/Model as follows (you may need to create the Model directory if it doesnt exist)
class Model_TaskType extends Model_Table {
public $entity_code='tasktype';
public $table_alias='ty';
function defineFields(){
parent::defineFields();
$this->newField('id')
->mandatory(true);;
$this->newField('tasktype_desc')
->mandatory(true);
$this->newField('budget_code')
->mandatory(true);
}
public function afterInsert($new_id){
$ttb=$this->add('Model_TaskTypeBudget');
$ttb->set('id',$new_id)
->set('budget_code',$this->get('budget_code'));
$ttb->insert();
return $this;
}
public function beforeUpdate(&$data){
$ttb=$this->add('Model_TaskTypeBudget')->loadData($data['id']);
$ttb->set('budget_code', $data['budget_code']);
$ttb->update();
return $this;
}
public function beforeDelete(&$data){
$ttb=$this->add('Model_TaskTypeBudget')->loadData($data['id']);
$ttb->delete();
return $this;
}
}
Note if you are using innoDB and have foreign keys, you have to do the insert and delete in the right order e.g. if there was a foreign key from TaskTypeBudget to TaskType on ID, then it should use beforeDelete and afterInsert to prevent constraint violations.
class Model_TaskTypeBudget extends Model_Table {
public $entity_code='tasktype_budget';
public $table_alias='tyb';
function defineFields(){
parent::defineFields();
$this->newField('id')
->mandatory(true);
$this->newField('budget_code')
->mandatory(true);
}
}
And a page in ATKHOME/page like this
class page_tasktype extends Page {
function init(){
parent::init();
$p=$this;
$tt=$this->add('Model_TaskType');
$crud=$p->add('CRUD');
$crud->setModel($tt, array('id','tasktype_desc', 'budget_code'));
if($crud->grid)
$crud->grid->addPaginator(10);
}
}
Note also be careful to include an opening < ? php tag before each class line but DO NOT include a closing ? > as this may cause errors in the Ajax.
In ATKHOME/config-default.php, modify the mysql connection username and password from root/root to the user and password of your mysql database.
$config['dsn']='mysql://atktest:atktest#localhost/atktest';
and modify ATKHOME/lib/Frontend.php to uncomment line 8 which allows all pages to connect to the database (you can also just add the $this->dbConnect(); line to the page.
class Frontend extends ApiFrontend {
function init(){
parent::init();
$this->dbConnect(); //uncommented
In the same Frontend.php, insert the following around line 50 to add a button to the default menu to add our new page.
->addMenuItem('CRUD Test', 'tasktype')
Now go to your webbrowser and enter http://localhost/agiletoolkit and on the first page, click CRUD Test. Adding rows will result in a row being added to TASKTYPE and a row with the same id and budget_code to TASKTYPE_BUDGET. Editing the budget_code will be reflected in both tables and deleting the row will delete it from both tables.
How neat and simple is that once you know the functions are provided by ATk4 ?

Categories