I'm writing a behavior in CakePHP 3.0.11, and I can var_dump() and Log::debug() the $event object in my behavior's beforeSave() method, and can see the object data, but cannot access the object's properties.
I am simply trying to get the table (alias, className, tablename, anything) name from the object.
I would like to do something like this in my Behavior:
public function beforeSave(Event $event, Entity $entity)
{
$table = $event->_alias;
// etc.
}
I tried the event's subject() method which extracts the table object from the event,
$table = $event->subject();
When I var_dump or debug the object returned, it shows:
Debug: App\Model\Table\CompaniesTable Object
(
[registryAlias] => Companies
[table] => companies
[alias] => Companies
[entityClass] => App\Model\Entity\Company
[associations] => Array
(
[0] => defaultshippingusers
(...)
[defaultConnection] => default
[connectionName] => default
)
But I cannot access 'table', 'alias', etc. from my $table object.
When I do, I get a fatal error:
Table Companies is not associated with 'alias'
Is there a simple way to get the Table name from the $event object in a behavior?
Dumping objects does not necessarily give you a representation of the objects structure, but custom formatted debug information, defined via the magic __debugInfo() method.
https://github.com/cakephp/cakephp/blob/3.0.11/src/ORM/Table.php#L2190
Table classes do not have table or alias properties, but methods with the same name, just have a look at the Cookbook and the API docs.
$alias = $event->subject()->alias();
Cookbook > Database Access & ORM > Table Objects > Basic Usage
API > \Cake\ORM\Table
API > \Cake\ORM\Table::alias()
API > \Cake\ORM\Table::table()
...
You can also get the table name or alias directly from the behaviour without using the $event object:
$this->getTable()->table();
$this->getTable()->alias();
This is useful if you add your own functions to the behaviour which don't pass in the $event.
Related
I am working on Restful APIs of Yii.
My controller name is ProductsController and the Model is Product.
When I call API like this GET /products, I got the listing of all the products.
But, now I want to filter the records inside the listing API.
For Example, I only want those records Which are having a product name as chairs.
How to implement this?
How to apply proper filtering on my Rest API. I am new to this. So, I have no idea how to implement this. I also followed their documentation but unable to understand.
May someone please suggest me a good example or a way to achieve this?
First of all you need to have validation rules in your model as usual.
Then it's the controllers job and depending on the chosen implementation I can give you some hints:
If your ProductsController extends yii\rest\ActiveController
Basically the easiest way because almost everything is already prepared for you. You just need to provide the $modelClass there and tweak actions() method a bit.
public function actions()
{
$actions = parent::actions();
$actions['index']['dataFilter'] = [
'class' => \yii\data\ActiveDataFilter::class,
'searchModel' => $this->modelClass,
];
return $actions;
}
Here we are modifying the configuration for IndexAction which is by default responsible for GET /products request handling. The configuration is defined here and we want to just add dataFilter key configured to use ActiveDataFilter which processes filter query on the searched model which is our Product. The other actions are remaining the same.
Now you can use DataProvider filters like this (assuming that property storing the product's name is name):
GET /products?filter[name]=chairs will return list of all Products where name is chairs,
GET /products?filter[name][like]=chairs will return list of all Products where name contains word chairs.
If your ProductsController doesn't extend yii\rest\ActiveController but you are still using DataProvider to get collection
Hopefully your ProductsController extends yii\rest\Controller because it will already benefit from serializer and other utilities but it's not required.
The solution is the same as above but now you have to add it by yourself so make sure your controller's action contains something like this:
$requestParams = \Yii::$app->getRequest()->getBodyParams(); // [1]
if (empty($requestParams)) {
$requestParams = \Yii::$app->getRequest()->getQueryParams(); // [2]
}
$dataFilter = new \yii\data\ActiveDataFilter([
'searchModel' => Product::class // [3]
]);
if ($dataFilter->load($requestParams)) {
$filter = $dataFilter->build(); // [4]
if ($filter === false) { // [5]
return $dataFilter;
}
}
$query = Product::find();
if (!empty($filter)) {
$query->andWhere($filter); // [6]
}
return new \yii\data\ActiveDataProvider([
'query' => $query,
'pagination' => [
'params' => $requestParams,
],
'sort' => [
'params' => $requestParams,
],
]); // [7]
What is going on here (numbers matching the code comments):
We are gathering request parameters from the body,
If these are empty we take them from the URL,
We are preparing ActiveDataFilter as mentioned above with searched model being the Product,
ActiveDataFilter object is built using the gathered parameters,
If the build process returns false it means there is an error (usually unsuccessful validation) so we return the object to user to see list of errors,
If the filter is not empty we are applying it to the database query for Product,
Finally we are configuring ActiveDataProvider object to return the filtered (and paginated and sorted if applicable) collection.
Now you can use DataProvider filters just as mentioned above.
If your ProductsController doesn't use DataProvider to get collection
You need to create your custom solution.
Is their way, we can add MySQL custom function in Entity
protected $_virtual = ['check_tenant' => '(check_tenant(Tenants.id))'];
I would like to call following query in with find() method
//SELECT id, first_name, check_tenant(Tenants.id) FROM tenants AS Tenants
$this->Tenants->find()->all();
If I can define custom MySQL function in the virtual field then it would automatically return in the result set
I am able to pass the new field in select() method
$this->Tenants->find()
->select(['id', 'check_tenant' => '(check_tenant(Tenants.id))'])->all();
But i would like to define globally, so the new field don't need to pass in every find call
Virtual properties in CakePHP 3.x are not the same as virtual fields in CakePHP 2.x, the latter were used in SQL queries, and the former are being used on PHP level, usually with data already present in the entity.
If you want your custom field to be present in all queries, then you could for example use the Model.beforeFind() event to modify the queries accordingly:
// in TenantsTable class
public function beforeFind(\Cake\Event\Event $event, \Cake\ORM\Query $query, array $options)
{
return $query
// select custom fields (functions builder usage not required, but advised)
->select(function (\Cake\ORM\Query $query) {
return ['check_tenant' => $query->func()->check_tenant([
'Tenants.id' => 'identifier'
])];
})
// ensure that the tables default fields are being selected too
->enableAutoFields(true); // autoFields() before CakePHP 3.4
}
Another less invasive option would be custom finders, that you explicitly use where you need them:
// in TenantsTable class
public function findWithTenantCheck(\Cake\ORM\Query $query, array $options)
{
return $query
->select(/* ... */)
->enableAutoFields(true);
}
// query data
$query = $this->Tenants->find('withTenantCheck');
See also
Cookbook > Database Access & ORM > Table Objects > Lifecycle Callbacks > beforeFind
Cookbook > Database Access & ORM > Entities > Creating Virtual Properties
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Using Finders to Load Data
Cookbook > Database Access & ORM > Retrieving Data & Results Sets > Custom Finder Methods
Cookbook > Database Access & ORM > Query Builder > Using SQL Functions
I'm new to cakephp. I'm trying to search through mysql tables. I want to use nested query.
class TableController extends AppController{
.
.
public function show(){
$this->set('discouns', $this->DiscounsController->query("SELECT * FROM discoun as Discoun WHERE gcil_id = 1"));//(SELECT id FROM gcils WHERE genre = 'Shoes' AND company_name = 'Adidas')"));
}
}
Error:
Error: Call to a member function query() on a non-object
I've also tried
public function show(){
$this->DiscounsController->query("SELECT * FROM count as Count WHERE ctr_id = (SELECT id FROM ctrs WHERE genre = 'Shoes' AND company_name = 'Adidas')");
}
Error:
Error: Call to a member function query() on a non-object
File: C:\xampp\htdocs\cakephppro\myapp\Controller\CountsController.php
Please help. I've been trying this for last few hours. :/
As mentioned in the comments there are a few problems with your code.
Firstly, you are trying to call the query() method on a Controller, whereas you should be executing it on a Model, as it is models that handle database queries and the controller should simply be used to call these methods to get the data and pass them to the view.
The second thing is that you are executing a very simple SQL query raw instead of using CakePHPs built in functions <- Be sure to read this page in full.
Now for your problem, as long as you have setup your model relationships correctly and followed the correct naming conventions, this should be your code to run your SQL query from that controller:
public function show(){
$this->set('discouns', $this->Discouns->find('all', array(
'conditions' => array(
'gcil_id' => 1,
'genre' => 'shoes',
'company_name' => 'Adidas'
)
));
}
query() is not a Controller, but a Model method. That's what the error (Call to a member function on a non-object) is trying to tell you.
So the correct call would be:
$this->Discount->query()
But you are calling this in a TableController, so unless Table and Discount have some type of relationship, you won't be able to call query().
If the Table does have a relationship defined you will be able to call:
$this->Table->Discount->query()
Please not that query() is only used when performing complex SQL queries in scenarios where the standard methods (find, save, delete, etc.) are less practical.
$this->Counts->find('all',array(
'conditions' => array(
'ctrs.genre' => 'Shoes',
'ctrs.company_name' => 'Adidas'
), 'recursive' => 1
));
The above is with tables named counts and ctrs.
This is assuming you have the model set up to have some sort of relationship between the counts table and the ctrs table. It's kind of hard to tell in your code exactly what you tables are.
The CakePHP book should have all the answers you need. One of the reasons to run CakePHP over regular PHP is the FIND statement. Once you have your models set up correctly, using the find statement should be really easy.
http://book.cakephp.org/2.0/en/models.html
The validation on Kohana ORM is done using rules
function rules()
{
return array(
'username' => array(
array('not_empty'),
array(array($this, 'availability')),
)
);
}
I'm struggling to validate a JSON encoded column using $_serialize_columns.
class Model_Admin extends ORM {
protected $_belongs_to = array();
protected $_has_many = array(
'plans' => array(),
'groups' => array(),
'transactions' => array(),
'logins' => array()
);
protected $_serialize_columns = array('data');
/**
* #param array $data
* #param Validation $validation
*
* #return bool
*/
public function data($data, $validation)
{
return
Validation::factory(json_decode($data, TRUE))
// ... rules ...
->check();
}
public function rules()
{
return array(
'data' => array(
array(array($this, 'data'), array(':value',':validation')
)
);
}
}
the array that gets encoded is:
array(
'name' => '',
'address' => '',
'phone' => '',
'postalcode' => ''
);
the data method receives the json encoded data, because the ORM runs the filters before doing the validation, so I need to convert it back to an associative array, then create a new validation object to check specifically for the content of that array. Because I can't merge Validation rules from another Validation instance
Updated Answer
The use of a second validation object is necessary since save() causes the internal model validation object to be checked. This means that rules added to the validation object being checked from a validation rule will be ignored (Validation->check() imports the rules into local scope before looping).
Since the data itself is technically another object (in the sense of object relationships, it has its own dataset that needs validation) the ideal solution would be to find a way to create a real model that saves the data.
There are numerous other benefits to saving data with proper database column definitions, not least if you need to perform data property lookups, make in-situ changes etc. (which would otherwise require unserializing the data column, potetnailly in all rows).
There are some alternatives, but they feel like kludges to me:
Create a model that represents the data object and add rules to it, using check() to validate the data (problem: will require a lot of maintenance, no real-world table means columns must be manually defined).
Set the data as real columns in the Admin model, and use a filter that will convert it into the data column on set (problem: again, must manually define the columns and exclude the additional columns from the save operation).
I hope this is of some use.
Original Answer
The Kohana ORM save() method permits the inclusion of an "extra" validation object, which is merged into the main ORM validation object namespace.
This is documented briefly here.
If I have understood correctly, I think you are looking to do something like this:
// another script, e.g., a controller
// Create the model
$admin = ORM::factory('Admin');
// $data = the data as an array, before serialization ...
$extra_validation = Validation::factory($data)
// add ->rule() calls here, but DO NOT chain ->check()
;
// Set $data in the model if it is going to be saved, e.g., $admin->data = $data;
// Set other data... e.g., $admin->foo = 'bar';
// Save the model
try {
$admin->save($extra_validation);
}
catch (ORM_Validation_Exception $e)
{
// Manipulate the exception result
}
While in this example you must still create another validation object, you are now able to catch all exceptions in a single block. I would recommend using var_dump() or similar on $e->errors() to check the namespace if you are using i18n messages to provide a human-readable error message. You should find that a namespace called "_external" has been created in the response.
I looked through the file Mage/Catalog/sql/catalog_setup/install-1.6.0.0.php.
The part of code:
$installer = $this;
/* #var $installer Mage_Catalog_Model_Resource_Setup */
$installer->startSetup();
/**
* Create table 'catalog/product'
*/
$table = $installer->getConnection()
->newTable($installer->getTable('catalog/product'))
->addColumn('entity_id', Varien_Db_Ddl_Table::TYPE_INTEGER, null, array(
'identity' => true,
'unsigned' => true,
'nullable' => false,
'primary' => true,
), 'Entity ID')
You can see here catalog_product implementation: $installer->getTable('catalog/product').
But I couldn't find this table in DB.
How does it work then? I always thought that catalog/product = catalog_product.
The following function
getTable('catalog/product')
can be traced back to
app/code/core/Mage/Core/Model/Resource.php
checking the public function getTableName($modelEntity) you will see that the logic treats also resource table names:
<catalog_resource>
<class>Mage_Catalog_Model_Resource</class>
<deprecatedNode>catalog_resource_eav_mysql4</deprecatedNode>
<entities>
<product>
<table>catalog_product_entity</table>
</product>
more resources about this:
Magento ORM: Entity Attribute Value; Part 1 and
Magento Setup Resources from Alan Storm
As is often the case in Magento, configuration is being used. Here's the call stack:
Mage_Core_Model_Resource_Setup::getTable('catalog/product')
Mage_Core_Model_Resource::getTableName('catalog/product');
When a '/' is present in the argument passed to the core/resource class's getTableName method, the configuration DOM is inspected. First the method will resolve the resourceModel node with the following line:
$resourceModel = (string) Mage::getConfig()->getNode()->global->models->{$model}->resourceModel;
Then, the core/resource class calls its getEntity() method, with the resourceModel node passed as the argument. This method simply looks under the resolved (resource) model node for the entity declaration (i.e. tablename):
Mage::getConfig()->getNode()->global->models->{$model}->entities->{$entity};
In the case of catalog/product, the above maps to:
Mage::getConfig()->getNode()->global->models->catalog_resource->entities->product;
If you look in Mage_Catalog's configuration xml, you'll see this borne out. The reason why it is best to access the tablename via configuration is that it is possible to specify table prefix, and using this method will return the correct name.