I have an issue where I filter a collection by date and the item I expect to get is not being returned in the collection, however if I print out the SQL that the collection uses and run that against my database the item is returned.
$from = new \DateTime($lsDate);
$orders = $this->_orderCollectionFactory->create()
->addFieldToSelect(['grand_total', 'created_at'])
->addAttributeToFilter('created_at', array('gteq' => $from->format('Y-m-d H:i:s')))
->addAttributeToFilter('customer_id',$customer->getId())
->setPageSize(10)
->setOrder('created_at', 'desc');
$from->format('Y-m-d H:i:s') // Lets say this is 2019-08-06 15:33:00
$this->logger->info(count($orders)); // This is 0
If I print out the SQL that this generates it looks something like this:
SELECT `main_table`.`entity_id`, `main_table`.`grand_total`, `main_table`.`created_at` FROM `sales_order` AS `main_table` WHERE (`created_at` >= '2019-08-06 15:33:21')
The orders created_at
date that should be returned is 2019-08-06 15:34:00
.
If i run the above query on my database it returns the one order above however as you can see in my code above the collection is empty.
If i change the date of the order to be 2019-08-06 16:34:21
(one hour in the future) the code then returns a collection with one item. It looks like it has something to do with the timezone somwehere? Maybe DST (Daylight Saving Time)?
EDIT
Here is some more information about the $lsDate
variable.
$lsDate
comes from a customer attribute. I store the date as so:
$newDate = new \DateTime();
$customer->setCustomAttribute('ls_start_date', $newDate->format('Y-m-d H:i:s'));
And get the date as so:
$lsDate = $customer->getCustomAttribute('ls_start_date')->getValue();
To start with, maybe we need a general Magento Date handling explanation.
Magento intend all its dates to be saved in DB in GMT.
The reason for this design choice is quite simple: Magento allows you to configure multi-store that could be in multiples timezones.
Let's imagine I have a Magento with 3 stores, that I do operate from London.
Here are my store:
Europe/London
; this is also my Main Store configurationAsia/Tokyo
timezoneAmerica/New_York
And now, let's take a business case were I do promise my customer that "We deliver in 48 hour, worldwilde, based on the timezone of the capital city of the country you live in.".
Then I get 3 orders, ones for each stores, all of them on the 1st of May at 16:15.
That would be extremly unconveniant for me, in the admin, to have all the three orders stated as placed on the 1st May at 16:15, to fulfill what I promise to my customer , because I would have to do crazy calculation based on the store I see in the admin grid of the orders.
The best for me would be to see
Magento, in order to do that, would retrieve the date in GMT from the database and then just apply the current sotre timezone to the date.
Quite easy.
Imagine the complexity it would have be if they did store timezoned dates in the database... Magento would need to store both the date AND the timezone and do conversions back and forth or timezones computation for any single date you have to display.
Pretty crazy job to do.
So your best bet, in order to follow Magento's way of working would be to store your dates in the database in GMT, and so to create your customer date this way:
use Magento\Framework\Stdlib\DateTime\DateTimeFactory;
use Magento\Customer\Model\Customer;
class CustomerLsDate {
private $dateTimeFactory;
public function __construct(DateTimeFactory $dateTimeFactory) {
$this->dateTimeFactory = $dateTimeFactory;
}
public function setLsDate(Customer $customer): CustomerLsDate {
$customer->setCustomAttribute('ls_start_date', $this->dateTimeFactory->create()->gmtDate('Y-m-d H:i:s'));
return $this;
}
}
Then, when you want to query on this date, just use it as is.
If you want to diplsay it to someone, in the store timezone, then:
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Customer\Model\Customer;
class CustomerLsDate {
private $timezone;
public function __construct(TimezoneInterface $timezone) {
$this->timezone = $timezone;
}
public function getLsDate(Customer $customer): string {
$date = $this->timezone->date(
new \DateTime(
$customer->getCustomAttribute('ls_start_date')->getValue(),
new \DateTimeZone('GMT')
)
);
Zend_Debug::dump($date->format('Y-m-d H:i:s'));
return $date->format('Y-m-d H:i:s');
}
}
That would really be the approach fitting most with Magento philosophy
Full CustomerLsDate
class:
use Magento\Framework\Stdlib\DateTime\DateTimeFactory;
use Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use Magento\Customer\Model\Customer;
class CustomerLsDate {
private $dateTimeFactory;
private $timezone;
public function __construct(DateTimeFactory $dateTimeFactory, TimezoneInterface $timezone) {
$this->timezone = $timezone;
$this->dateTimeFactory = $dateTimeFactory;
}
public function setLsDate(Customer $customer): CustomerLsDate {
$customer->setCustomAttribute(
'ls_start_date',
$this->dateTimeFactory->create()->gmtDate('Y-m-d H:i:s')
);
return $this;
}
public function getLsDate(Customer $customer): string {
$date = $this->timezone->date(
new \DateTime(
$customer->getCustomAttribute('ls_start_date')->getValue(),
new \DateTimeZone('GMT')
)
);
Zend_Debug::dump($date->format('Y-m-d H:i:s'));
return $date->format('Y-m-d H:i:s');
}
}
Rick James have part of the answer.
Since created_at
is a timestamp
and that a default connection to MySQL will apply the server timezone to a timestamp, your manual query works.
But now if you go like Magento and do
SET time_zone = '+00:00';
SELECT `main_table`.`entity_id`, `main_table`.`grand_total`, `main_table`.`created_at` FROM `sales_order` AS `main_table` WHERE (`created_at` >= '2019-08-06 15:33:21');
Your query won't return any result like your Magento collection does.
The time_zone
setting of Magento is done in their default PDO adapter implementation here:
/**
* Creates a PDO object and connects to the database.
*
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
* @SuppressWarnings(PHPMD.NPathComplexity)
*
* @return void
* @throws \Zend_Db_Adapter_Exception
* @throws \Zend_Db_Statement_Exception
*/
protected function _connect()
{
// extra unrelated code comes here...
// As we use default value CURRENT_TIMESTAMP for TIMESTAMP type columns we need to set GMT timezone
$this->_connection->query("SET time_zone = '+00:00'");
// extra unrelated code comes here...
}
Source: Magento/Framework/DB/Adapter/Pdo/Mysql
From there on, your answer lies in where your variable $lsDate
is coming from and if you are able to know its timezone, in order to translate it back to GMT, to have the correct GMT date to give to your collection filter.
For example, if you know that your timezone is 'Europe/London'
you can do
$date = new \DateTime('2019-08-06 15:33:21', new \DateTimeZone('Europe/London'));
$date->setTimezone(new \DateTimeZone('GMT'));
echo $date->format('Y-m-d H:i:s'); // echoes 2019-08-06 14:33:21
And from your edit, when you do create a new \DateTime()
you will get the a DateTime
bound to the timezone of your server.
So based on your liking you can either save the date in your custom customer field in GMT, or save the timezone as well as the date.
Either the PHP way
$newDate = new \DateTime('now',new \DateTimeZone('GMT'));
$customer->setCustomAttribute('ls_start_date', $newDate->format('Y-m-d H:i:s'));
And you'll end up having a GMT date on you customer ls_start_date
Or you can also do it more Magento way, with DI:
use Magento\Framework\Stdlib\DateTime\DateTimeFactory;
class Whatever {
private $dateTimeFactory;
public function __construct(DateTimeFactory $dateTimeFactory) {
$this->dateTimeFactory = $dateTimeFactory;
}
public function assignThatLsDate($customer) {
$customer->setCustomAttribute('ls_start_date', $this->dateTimeFactory->create()->gmtDate('Y-m-d H:i:s'));
}
}
$newDate = new \DateTime();
$customer->setCustomAttribute('ls_start_date', $newDate->format('Y-m-d H:i:s'));
$customer->setCustomAttribute('ls_start_date_timezone', $newDate->getTimezone ());
Then
$from = new \DateTime(
$customer->getCustomAttribute('ls_start_date')->getValue(),
$customer->getCustomAttribute('ls_start_date_timezone')->getValue()
)->setTimezone(new \DateTimeZone('GMT'));
// query to your collection is unchanged
(too complex for a Comment; may lead to an answer.)
In MySQL, do
SHOW VARIABLES LIKE "%zone%"; -- looking for how the timezone is set
SHOW CREATE TABLE ... -- looking for datatypes used
In particular, is created_at
a DATETIME
or a TIMESTAMP
?
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With