I cant save multiple records in cakephp3 v3.2. I have a field to reset to 0 on about 20 rows of data (see below). The below code doesnt save. I dont get an error and a debug of the data is correct but nothing happens, no errors. Just it doesnt reset that field to 0.
I can get it to work using execute method as below but why doesnt newentites work?
private function tmpschedules( $tmpscheduleStudents=0, $tutorId=0){
foreach ( $tmpscheduleStudents as $key => $item) :
// debug($item);
$tmpscheduleStudents[$key]['allocated']=0;
endforeach;
// exit;
$students_schedule = $this->Tmpschedules->newEntities($tmpscheduleStudents, ['validate' => false]);
foreach ( $students_schedule as $key => $item) {
debug($item);
$result=$this->Tmpschedules->save($item, ['atomic' => false]);
}
exit;
return 1;
}
//see allocated field is 0 before being saved but it doesnt save.
object(App\Model\Entity\Tmpschedule) {
'student_id' => (int) 1039,
'tutor_id' => (int) 92,
'rank' => (int) 0,
'inactive' => (int) 0,
'weekday_start' => (int) 0,
'start_order' => (int) 0,
'allocated' => (int) 0,
//this does work
foreach ( $tmpscheduleStudents as $key => $item) :
// debug($item);
// $tmpscheduleStudents[$key]['allocated']=0;
$query = $this->Tmpschedules->query();
$query->update()
->set(['allocated' => 0])
->where(['tutor_id' => $tutorId])
->execute();
endforeach;
//result update which has allocated as o0 but doesnt save and no errors as you see below
'student_id' => (int) 245,
'tutor_id' => (int) 13,
'rank' => (int) 0,
'inactive' => (int) 0,
'weekday_start' => (int) 0,
'start_order' => (int) 0,
'allocated' => (int) 0,
'[new]' => true,
'[accessible]' => [
'*' => true
],
'[dirty]' => [
'student_id' => true,
'tutor_id' => true,
'rank' => true,
'inactive' => true,
'weekday_start' => true,
'start_order' => true,
'allocated' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Tmpschedule2s'
}
What is the field type? If the field type and the value you are inserting do not match it will be getting marshaled away. My guess is that is what is happening. The key difference is in one of your examples you are casting and in the other you are not casting also in some you use 0 vs false. Keep consistent.
The following should work
$students_schedule = $this->Tmpschedules->newEntities($tmpscheduleStudents, ['validate' => 0]);
You could also get away with
$students_schedule = $this->Tmpschedules->newEntities($tmpscheduleStudents, ['validate' => (int)false]);
just make sure that your column type and value type match and that should resolve your issue. My example assumes you are using a numeric type field.
Related
I am trying to retrieve records from a primary and associated model where the associated model has a condition on it. The is a basic one to many relationship. The WpPosts is the primary and i want all associated rows from WpPostmetas that have the meta_value of lead_data from a data range. The below code does this but it also including null associated models to the WpPost output. The below data should not have been retrieved. How do i prevent the below data from being retrieved?
object(App\Model\Entity\WpPost) {
'ID' => (int) 1997,
'post_author' => (int) 1,
'post_date' => object(Cake\I18n\FrozenTime) {
'time' => '2018-09-27T12:08:40+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'post_date_gmt' => object(Cake\I18n\FrozenTime) {
'time' => '2018-09-27T12:08:40+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
.....
'comment_count' => (int) 0,
'wp_postmetas' => [], //see null value should be included
///query works but also included null associated models
$q2= $this->WpPosts->find()->contain('WpPostmetas', function ($q) {
return $q
->where(['meta_key LIKE' => '%lead_data%' ]);
})
->where(['WpPosts.post_date >=' => $searchDate ])
->order(['WpPosts.id' => 'DESC'])
->enableHydration(true);
I am trying to order data in accordance with its priority...
But this query is not working for me....
I cannot figure it out where it went wrong..
please help me to solve this..
$admins = $this->xxx->find()
->select($fields)
->where($conditions)
->contain([
'yyy' => function ($q) {
return $q->autoFields(false)
->select(['id','name','login_url','priority'])
->order(['priority' => 'ASC']);
}
])
->all();
If the realtionship between xxx and yyy is belongsTo then you have to move the order() method outside the contain
$admins = $this->xxx->find()
->select($fields)
->where($conditions)
->contain([
'yyy' => function ($q) {
return $q->autoFields(false)
->select(['id','name','login_url','priority']);
}
])
->order(['yyy.priority' => 'ASC'])
->all();
array(
'conditions' => array('Model.field' => $thisValue), //array of conditions
'recursive' => 1, //int
//array of field names
'fields' => array('Model.field1', 'DISTINCT Model.field2'),
//string or array defining order
'order' => array('Model.created', 'Model.field3 DESC'),
'group' => array('Model.field'), // fields to GROUP BY
'limit' => n, //int
'page' => n, //int
'offset' => n, //int
'callbacks' => true //other possible values are false, 'before', 'after'
'having' => array('COUNT(Model.field) >' => 1), // fields to HAVING by
'lock' => true // Enable FORM UPDATE locking
)
Using the POST method $data = $this->request->getData(); , I get the archive:
[
'category_id' => '62',
'title' => 'Name-1',
'body' => '<p>Text</p>
',
'price' => '30',
'is_new' => '1',
'img' => [
'tmp_name' => 'D:\Web\OpenServer\userdata\temp\php70D9.tmp',
'error' => (int) 0,
'name' => 'IronMan.jpg',
'type' => 'image/jpeg',
'size' => (int) 131830
]
]
By preparing these data for the record in the database:
$product = $this->Products->patchEntity($product, $data);
But the patchEntity() method cuts out all the information about the image.
I get:
object(App\Model\Entity\Product) {
'category_id' => (int) 62,
'title' => 'Name-1',
'body' => '<p>Text</p>
',
'price' => (float) 30,
'is_new' => (int) 1,
'img' => '', // <--- empty :(
'[new]' => true,
'[accessible]' => [
'category_id' => true,
'title' => true,
'body' => true,
'price' => true,
'img' => true,
'is_new' => true,
'created' => true,
'modified' => true,
'category' => true
],
'[dirty]' => [
'category_id' => true,
'title' => true,
'body' => true,
'price' => true,
'is_new' => true,
'img' => true
],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Products'
}
It can be fixed? Tell me at least about. Thank you.
When patching/creating an entity, the data is bein marshalled according to the respective columns data type, as you can see for other properties like price, which is converted from a string to a float.
Your img column is probably of the type string, causing the marshaller to convert the data accordingly (see \Cake\Database\Type\StringType::marshal()).
There are various ways to avoid that, for example using a different property name that doesn't map to an existing column, like img_upload, and then after moving the upload, manually set the resulting filesystem path to the img property and save that.
That could also be done in the beforeMarshal event in your ProductsTable class, so that the view template can continue to use the img property:
public function beforeMarshal(
\Cake\Event\Event $event,
\ArrayObject $data,
\ArrayObject $options
) {
if (isset($data['img'])) {
$data['img_upload'] = $data['img'];
unset($data['img']);
}
}
You could also create a custom database type for the img column, one which doesn't marshal the data to a string, but just passes it on:
namespace App\Database\Type;
use Cake\Database\Type;
class FileType extends Type
{
public function marshal($value)
{
return $value;
}
}
You'd have to assign the filesystem path anyways though, you'd basically just avoid using a separate/temporary property.
See also
Cookbook > Database Access & ORM > Saving Data > Modifying Request Data Before Building Entities
Cookbook > Database Access & ORM > Database Basics > Adding Custom Types
I do not know how much this is correct, but in the end I did the following and everything works as I need:
In ProductsController:
public function add()
{
$product = $this->Products->newEntity();
if ($this->request->is('post')) {
$data = $this->request->getData();
$product = $this->Products->patchEntity($product, $data);
// If there is a picture that we checked (by the method of validationDefault, when calling patchEntity) and have already uploaded to the server in a temporary folder, then
if($product->img_upload['name']){
// We call the user method of processing the downloaded image
$product = $this->_customUploadImg($product);
// Leave only the name of the new file, adding it to the new property, with the name corresponding to the name of the table in the database
$product->img = $product->img_upload['name'];
}
// Delete an unnecessary property
unset($product->img_upload);
if ($this->Products->save($product)) {
// ...
}
// ...
}
In Product.php:
class Product extends Entity{
protected $_accessible = [
'category_id' => true,
'title' => true,
'body' => true,
'price' => true,
'is_new' => true,
'created' => true,
'modified' => true,
'category' => true,
// 'img' => true,
// Here we specify not 'img' as in the database table, but 'img_upload', in order for ProductsController not to delete our data file about the uploaded file when patchEntity was called.
'img_upload' => true,
];
}
In ProductsTable.php:
public function validationDefault(Validator $validator)
{
//...
$validator
->allowEmpty('img_upload', 'create')
->add('img_upload', [
'uploadError' => [
'rule' => 'uploadError',
'message' => 'Error loading picture'
],
'mimeType' => [
'rule' => ['mimeType', ['image/jpeg', 'image/jpg', 'image/png', 'image/gif']],
'message' => 'Only image files are allowed to be uploaded: JPG, PNG и GIF'
],
'fileSize' => [
'rule' => ['fileSize', '<=', '2MB'],
'message' => 'The maximum file size should be no more than 2 MB'
]
]);
//...
}
In add.ctp:
echo $this->Form->create($product, ['type' => 'file']) ?>
// ...
echo $this->Form->control('img_upload', ['type' => 'file', 'label'=>'Product photo']);
//...
Thanks "ndm" and "mark"!
Events hasMany TicketTypes
TicketTypes belogsTo Events
I am trying to retrieve all events with associated ticket types:
$query= $this->Events
->find()
->select(['id', 'name'])
->autoFields(false)
->contain(['TicketTypes' => function($q) {
return $q->select(['TicketTypes.id', 'TicketTypes.name']); }])
;
SQL query generated:
SELECT Events.id AS `Events__id`, Events.name AS `Events__name` FROM events Events
But what I expected is:
SELECT Events.id AS `Events__id`, Events.name AS `Events__name`, TicketTypes.id AS `TicketTypes__id`, TicketTypes.name AS `TicketTypes__name` FROM events Events LEFT JOIN ticket_types TicketTypes ON Events.id = (TicketTypes.event_id)
This is how my models are configured:
class EventsTable extends Table
{
public function initialize(array $config)
{
$this->displayField('name');
$this->addAssociations([
'hasMany'=> ['TicketTypes']
]);
}
}
class TicketTypesTable extends Table
{
public function initialize(array $config)
{
$this->displayField('name');
$this->addAssociations([
'belongsTo' => ['Events']
]);
}
}
Here is the result of debugging my find query:
object(Cake\ORM\Query) {
'(help)' => 'This is a Query object, to get the results execute or iterate it.',
'sql' => 'SELECT Events.id AS `Events__id`, Events.name AS `Events__name` FROM events Events',
'params' => [],
'defaultTypes' => [
'Events.id' => 'integer',
'id' => 'integer',
'Events.name' => 'string',
'name' => 'string',
'Events.datetime_start' => 'datetime',
'datetime_start' => 'datetime',
'Events.datetime_end' => 'datetime',
'datetime_end' => 'datetime',
'Events.created' => 'datetime',
'created' => 'datetime',
'Events.modified' => 'datetime',
'modified' => 'datetime',
'Events.slug' => 'string',
'slug' => 'string',
'TicketTypes.id' => 'integer',
'TicketTypes.event_id' => 'integer',
'event_id' => 'integer',
'TicketTypes.name' => 'string',
'TicketTypes.description' => 'text'
],
'decorators' => (int) 0,
'executed' => false,
'hydrate' => true,
'buffered' => true,
'formatters' => (int) 0,
'mapReducers' => (int) 0,
'contain' => [
'TicketTypes' => [
'queryBuilder' => object(Closure) {
}
]
],
'matching' => [],
'extraOptions' => [],
'repository' => object(App\Model\Table\EventsTable) {
'registryAlias' => 'Events',
'table' => 'events',
'alias' => 'Events',
'entityClass' => 'App\Model\Entity\Event',
'associations' => [
(int) 0 => 'tickettypes'
],
'behaviors' => [],
'defaultConnection' => 'default',
'connectionName' => 'default'
}
}
And here is the result of debugging $query->all():
object(Cake\ORM\ResultSet) {
'items' => [
(int) 0 => object(App\Model\Entity\Event) {
'id' => (int) 101,
'name' => 'qwertyuiop',
'ticket_types' => [],
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[repository]' => 'Events'
},
...
As you can see in this line 'ticket_types' => [] ticket types are not being returned by the query.
What can I do to retrieve TicketTypes data?
Thanks.
hasMany associations are being retrieved in a separate query
Your assumption about how the CakePHP ORM retrieves associated data is incorrect.
Unlike hasOne and belongsTo assocaitions which are using joins in the main query, hasMany and belongsToMany asociated data is being retrieved in a separate query, which is being filtered using foreign key values collected from the main query, which in your case would be the Events.id column values.
Look at the rest of the SQL log, you shound find a query similar to
SELECT
TicketTypes.id AS `TicketTypes__id`, ...
FROM
ticket_types TicketTypes
WHERE
TicketTypes.event_id IN (1,2,3, ...)
The results of that query are being stitched together with the main results, and returned in a single result set.
Foreign keys need to be selected
A second problem is that your containments select() call is missing the foreign key column (TicketTypes.event_id), which is required, as without it, the ORM cannot stitch the results together, and thus the ticket types will not be present in the results.
Quote from the docs:
When you limit the fields that are fetched from an association, you
must ensure that the foreign key columns are selected. Failing to
select foreign key fields will cause associated data to not be present
in the final result.
See also
Cookbook > Database Access & ORM > Retrieving Associated Data
Cookbook > Database Access & ORM > Query Builder > Passing Conditions to Contain
I want to get count of tenancies based on differnet times
$start_date = $end_date = $end_date1 = new \DateTime('first day of last month');
$start_date = $start_date->format('Y-m-01 00:00:00');
$end_date = $end_date->format('Y-m-t 23:59:59');
$end_date1 = $end_date1->format('Y-m-'. date('d') .' 23:59:59');
$start_date2 = $end_date2 = new \DateTime('now');
$start_date2 = $start_date2->format('Y-m-01 00:00:00');
$end_date2 = $end_date2->format('Y-m-d 23:59:59');
$tenancyDetails = $this
->find()
->select([
'total_last_month' => $this->find()->func()->count('Tenancy.id'),
'count_last_month' => $this->find()->func()->count('Tenancy1.id'),
'count_curr_month' => $this->find()->func()->count('Tenancy2.id'),
'Tenancy.created', 'Tenancy1.created', 'Tenancy2.created',
])
->leftJoin(['Tenancy1' => 'tenancy'],[
'Tenancy1.active' => 1,
'Tenancy1.stage !=' => 1,
'Tenancy1.company_id' => $id,
])
->leftJoin(['Tenancy2' => 'tenancy'],[
'Tenancy2.active' => 1,
'Tenancy2.stage !=' => 1,
'Tenancy2.company_id' => $id,
])
->where([
'Tenancy.active' => 1,
'Tenancy.stage !=' => 1,
'Tenancy.company_id' => $id,
])
->where(function ($exp, $q) use ($start_date, $end_date) {
return $exp->between('Tenancy.created', $start_date, $end_date);
})
->where(function ($exp, $q) use ($start_date, $end_date1) {
return $exp->between('Tenancy1.created', $start_date, $end_date1);
})
->where(function ($exp, $q) use ($start_date2, $end_date2) {
return $exp->between('Tenancy2.created', $start_date2, $end_date2);
})->toArray();
Here is what I get. The number of counts are the same. Eventhough the datesare different
[
(int) 0 => object(App\Model\Entity\Tenancy) {
'total_last_month' => (int) 4590,
'count_last_month' => (int) 4590,
'count_curr_month' => (int) 4590,
'created' => object(Cake\I18n\FrozenTime) {
'time' => '2016-03-01T10:57:21+00:00',
'timezone' => 'UTC',
'fixedNowTime' => false
},
'Tenancy1' => [
'created' => '2016-03-01 10:57:21'
],
'Tenancy2' => [
'created' => '2016-04-04 10:59:42'
],
'[new]' => false,
'[accessible]' => [
'*' => true
],
'[dirty]' => [],
'[original]' => [],
'[virtual]' => [],
'[errors]' => [],
'[invalid]' => [],
'[repository]' => 'Tenancy'
}
]
Even though I have written the query this way but still the same problem.
$query = $tenancies->find();
$query->select([
'total_last_month' => $query->func()->count('Tenancy.id'),
])
->where([
'Tenancy.active' => 1,
'Tenancy.stage !=' => 1,
'Tenancy.company_id' => $id,
])
->where(function ($exp) use ($start_date, $end_date) {
return $exp->between('Tenancy.created', $start_date, $end_date);
});
$query->select([
'count_last_month' => $query->func()->count('Tenancy1.id'),
])
->leftJoin(['Tenancy1' => 'tenancy'],[
'Tenancy1.company_id' => $id,
])
->where([
'Tenancy1.active' => 1,
'Tenancy1.stage !=' => 1,
])
->where(function ($exp) use ($start_date, $end_date1) {
return $exp->between('Tenancy1.created', $start_date, $end_date1);
});
I can see the printed query which is correct.
After looking at all the examples in the documentation it looks like you need to instantiate the ORM query builder instance before using the func() helpers.
http://book.cakephp.org/3.0/en/orm/query-builder.html#using-sql-functions
In theory $this->func() should work the same if the return is simply a string. but maybe using $this->func() results in a disconnect between the query instance you are trying to build and an unbound new instance?
try:
$tenancyDetails = $this>find();
$tenancyDetails->select([
'total_last_month' => $tenancyDetails->find()->func()->count('Tenancy.id'),
'count_last_month' => $tenancyDetails->find()->func()->count('Tenancy1.id'),
'count_curr_month' => $tenancyDetails->find()->func()->count('Tenancy2.id'),
'Tenancy.created',
'Tenancy1.created',
'Tenancy2.created',
])......
Edit
I checked the code for the count function in /ORM/query.php file.
If no values are set cake will perform the count() query on the call $this->func->count(). The PHP compiler must be reading and therefore executing the count query before the query has been built, meaning before your conditions have been set. That's why you you need to instantiate the class first to stop the count query executing prematurely.