As far as I can tell, this problem is very limited in scope, so you're likely to be best off by testing for one type of error, and fixing it.
All you want to do is make sure that adding "one month" to a late date like the 29th, 30th or 31st does not push you forward to the 1st, 2nd or 3rd of the next-next month.
The way date_modify() works (using it on an example date "2012-01-31" with a string like "+1 months"), is that it first increases the month number by 1, then finds the 31st day from the start of that month. This is why it spills over to March 3rd.
When this is not what you desired, all you have to do is use date_modify() again, now telling it to go back a few days (3 days in this example). Since you only want to go back to the last day of the previous month, the number of days you will want to go back is always the same as the day-of-month in your faulty date.
All that remains is to make sure you don't apply this correction when it is not needed, like when PHP were to improves in future. This is relatively easy, because the scope of the possible problem situations is very limited.
- (1) The problem only occurs when adding months to dates 29, 30 or 31
- (2) When the problem occurs, the resulting date is always 1, 2 or 3.
My code below adds "+1 month", checks if that has caused the day-of-month to change wildly from something high to something low, and adjusts the date if that's the case.
//Create the date, store its day-of-month, and add X months
$myDateTimeISO = "2012-01-31";
$addThese = 1;
$myDateTime = new DateTime($myDateTimeISO);
$myDayOfMonth = date_format($myDateTime,'j');
date_modify($myDateTime,"+$addThese months");
//Find out if the day-of-month has dropped
$myNewDayOfMonth = date_format($myDateTime,'j');
if ($myDayOfMonth > 28 && $myNewDayOfMonth < 4){
//If so, fix by going back the number of days that have spilled over
date_modify($myDateTime,"-$myNewDayOfMonth days");
}
echo date_format($myDateTime,"Y-m-d");
Results in: 2012-02-29 (yes, this was a leap year).
PS: If you want to add years, the problem and the symptoms are nearly identical. Again, you just need to to check if the day-of-month resulting is 1/2/3 and the day-of-month going in is 29/30/31. If so, you need to go back "-X days" using date_modify, where X is the resulting day-of-month.