Here's a cleaner test case that doesn't expire:
<?php
$origin = mktime(18, 0, 0, 7, 31, 2015);
var_dump( date('r', $origin), date('r', strtotime('-1 months', $origin)) );
string(31) "Fri, 31 Jul 2015 18:00:00 +0200"
string(31) "Wed, 01 Jul 2015 18:00:00 +0200"
I'm pretty sure it's a documentation issue, because the manual clearly states this (emphasis mine):
Relative month values are calculated based on the length of months
that they pass through. An example would be "+2 month 2011-11-30",
which would produce "2012-01-30". This is due to November being 30
days in length, and December being 31 days in length, producing a
total of 61 days.
... and it's wrong.
PHP bug tracker has tons of dupes about this. They're all closed as not a bug. Here's a relevant comment from 2009 that explains it:
I agree that this is an annoying behaviour.
Also, the implementation is problematic. Basically if you use '+1
month' it takes the month number, adds 1 and parses result as a new
date.
If you use '+1 month' on the first of the month, it sets the date to
the next first of the month.
This behaviour gives the impression, that php considers the length of
a month, which is not true.
But if you use '+1 month' on the last day of a month, the result is
unexpected as 2009-05-31 becomes 2009-06-31 which is an invalid date
and then interpreted as 2009-07-01.
This should at least be mentioned in the documentation.
-1 month
breaks on: codepad.viper-7.com/E4gP0W The 31st of a month don't surprise me, but the 29th and 30th of March do. – Incogitable