1*37748cd8SNickeau<?php 2*37748cd8SNickeau 3*37748cd8SNickeaunamespace Cron; 4*37748cd8SNickeau 5*37748cd8SNickeauuse DateTime; 6*37748cd8SNickeau 7*37748cd8SNickeau/** 8*37748cd8SNickeau * Day of month field. Allows: * , / - ? L W 9*37748cd8SNickeau * 10*37748cd8SNickeau * 'L' stands for "last" and specifies the last day of the month. 11*37748cd8SNickeau * 12*37748cd8SNickeau * The 'W' character is used to specify the weekday (Monday-Friday) nearest the 13*37748cd8SNickeau * given day. As an example, if you were to specify "15W" as the value for the 14*37748cd8SNickeau * day-of-month field, the meaning is: "the nearest weekday to the 15th of the 15*37748cd8SNickeau * month". So if the 15th is a Saturday, the trigger will fire on Friday the 16*37748cd8SNickeau * 14th. If the 15th is a Sunday, the trigger will fire on Monday the 16th. If 17*37748cd8SNickeau * the 15th is a Tuesday, then it will fire on Tuesday the 15th. However if you 18*37748cd8SNickeau * specify "1W" as the value for day-of-month, and the 1st is a Saturday, the 19*37748cd8SNickeau * trigger will fire on Monday the 3rd, as it will not 'jump' over the boundary 20*37748cd8SNickeau * of a month's days. The 'W' character can only be specified when the 21*37748cd8SNickeau * day-of-month is a single day, not a range or list of days. 22*37748cd8SNickeau * 23*37748cd8SNickeau * @author Michael Dowling <mtdowling@gmail.com> 24*37748cd8SNickeau */ 25*37748cd8SNickeauclass DayOfMonthField extends AbstractField 26*37748cd8SNickeau{ 27*37748cd8SNickeau /** 28*37748cd8SNickeau * Get the nearest day of the week for a given day in a month 29*37748cd8SNickeau * 30*37748cd8SNickeau * @param int $currentYear Current year 31*37748cd8SNickeau * @param int $currentMonth Current month 32*37748cd8SNickeau * @param int $targetDay Target day of the month 33*37748cd8SNickeau * 34*37748cd8SNickeau * @return \DateTime Returns the nearest date 35*37748cd8SNickeau */ 36*37748cd8SNickeau private static function getNearestWeekday($currentYear, $currentMonth, $targetDay) 37*37748cd8SNickeau { 38*37748cd8SNickeau $tday = str_pad($targetDay, 2, '0', STR_PAD_LEFT); 39*37748cd8SNickeau $target = DateTime::createFromFormat('Y-m-d', "$currentYear-$currentMonth-$tday"); 40*37748cd8SNickeau $currentWeekday = (int) $target->format('N'); 41*37748cd8SNickeau 42*37748cd8SNickeau if ($currentWeekday < 6) { 43*37748cd8SNickeau return $target; 44*37748cd8SNickeau } 45*37748cd8SNickeau 46*37748cd8SNickeau $lastDayOfMonth = $target->format('t'); 47*37748cd8SNickeau 48*37748cd8SNickeau foreach (array(-1, 1, -2, 2) as $i) { 49*37748cd8SNickeau $adjusted = $targetDay + $i; 50*37748cd8SNickeau if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) { 51*37748cd8SNickeau $target->setDate($currentYear, $currentMonth, $adjusted); 52*37748cd8SNickeau if ($target->format('N') < 6 && $target->format('m') == $currentMonth) { 53*37748cd8SNickeau return $target; 54*37748cd8SNickeau } 55*37748cd8SNickeau } 56*37748cd8SNickeau } 57*37748cd8SNickeau } 58*37748cd8SNickeau 59*37748cd8SNickeau public function isSatisfiedBy(DateTime $date, $value) 60*37748cd8SNickeau { 61*37748cd8SNickeau // ? states that the field value is to be skipped 62*37748cd8SNickeau if ($value == '?') { 63*37748cd8SNickeau return true; 64*37748cd8SNickeau } 65*37748cd8SNickeau 66*37748cd8SNickeau $fieldValue = $date->format('d'); 67*37748cd8SNickeau 68*37748cd8SNickeau // Check to see if this is the last day of the month 69*37748cd8SNickeau if ($value == 'L') { 70*37748cd8SNickeau return $fieldValue == $date->format('t'); 71*37748cd8SNickeau } 72*37748cd8SNickeau 73*37748cd8SNickeau // Check to see if this is the nearest weekday to a particular value 74*37748cd8SNickeau if (strpos($value, 'W')) { 75*37748cd8SNickeau // Parse the target day 76*37748cd8SNickeau $targetDay = substr($value, 0, strpos($value, 'W')); 77*37748cd8SNickeau // Find out if the current day is the nearest day of the week 78*37748cd8SNickeau return $date->format('j') == self::getNearestWeekday( 79*37748cd8SNickeau $date->format('Y'), 80*37748cd8SNickeau $date->format('m'), 81*37748cd8SNickeau $targetDay 82*37748cd8SNickeau )->format('j'); 83*37748cd8SNickeau } 84*37748cd8SNickeau 85*37748cd8SNickeau return $this->isSatisfied($date->format('d'), $value); 86*37748cd8SNickeau } 87*37748cd8SNickeau 88*37748cd8SNickeau public function increment(DateTime $date, $invert = false) 89*37748cd8SNickeau { 90*37748cd8SNickeau if ($invert) { 91*37748cd8SNickeau $date->modify('previous day'); 92*37748cd8SNickeau $date->setTime(23, 59); 93*37748cd8SNickeau } else { 94*37748cd8SNickeau $date->modify('next day'); 95*37748cd8SNickeau $date->setTime(0, 0); 96*37748cd8SNickeau } 97*37748cd8SNickeau 98*37748cd8SNickeau return $this; 99*37748cd8SNickeau } 100*37748cd8SNickeau 101*37748cd8SNickeau /** 102*37748cd8SNickeau * Validates that the value is valid for the Day of the Month field 103*37748cd8SNickeau * Days of the month can contain values of 1-31, *, L, or ? by default. This can be augmented with lists via a ',', 104*37748cd8SNickeau * ranges via a '-', or with a '[0-9]W' to specify the closest weekday. 105*37748cd8SNickeau * 106*37748cd8SNickeau * @param string $value 107*37748cd8SNickeau * @return bool 108*37748cd8SNickeau */ 109*37748cd8SNickeau public function validate($value) 110*37748cd8SNickeau { 111*37748cd8SNickeau // Allow wildcards and a single L 112*37748cd8SNickeau if ($value === '?' || $value === '*' || $value === 'L') { 113*37748cd8SNickeau return true; 114*37748cd8SNickeau } 115*37748cd8SNickeau 116*37748cd8SNickeau // If you only contain numbers and are within 1-31 117*37748cd8SNickeau if ((bool) preg_match('/^\d{1,2}$/', $value) && ($value >= 1 && $value <= 31)) { 118*37748cd8SNickeau return true; 119*37748cd8SNickeau } 120*37748cd8SNickeau 121*37748cd8SNickeau // If you have a -, we will deal with each of your chunks 122*37748cd8SNickeau if ((bool) preg_match('/-/', $value)) { 123*37748cd8SNickeau // We cannot have a range within a list or vice versa 124*37748cd8SNickeau if ((bool) preg_match('/,/', $value)) { 125*37748cd8SNickeau return false; 126*37748cd8SNickeau } 127*37748cd8SNickeau 128*37748cd8SNickeau $chunks = explode('-', $value); 129*37748cd8SNickeau foreach ($chunks as $chunk) { 130*37748cd8SNickeau if (!$this->validate($chunk)) { 131*37748cd8SNickeau return false; 132*37748cd8SNickeau } 133*37748cd8SNickeau } 134*37748cd8SNickeau 135*37748cd8SNickeau return true; 136*37748cd8SNickeau } 137*37748cd8SNickeau 138*37748cd8SNickeau // If you have a comma, we will deal with each value 139*37748cd8SNickeau if ((bool) preg_match('/,/', $value)) { 140*37748cd8SNickeau // We cannot have a range within a list or vice versa 141*37748cd8SNickeau if ((bool) preg_match('/-/', $value)) { 142*37748cd8SNickeau return false; 143*37748cd8SNickeau } 144*37748cd8SNickeau 145*37748cd8SNickeau $chunks = explode(',', $value); 146*37748cd8SNickeau foreach ($chunks as $chunk) { 147*37748cd8SNickeau if (!$this->validate($chunk)) { 148*37748cd8SNickeau return false; 149*37748cd8SNickeau } 150*37748cd8SNickeau } 151*37748cd8SNickeau 152*37748cd8SNickeau return true; 153*37748cd8SNickeau } 154*37748cd8SNickeau 155*37748cd8SNickeau // If you contain a /, we'll deal with it 156*37748cd8SNickeau if ((bool) preg_match('/\//', $value)) { 157*37748cd8SNickeau $chunks = explode('/', $value); 158*37748cd8SNickeau foreach ($chunks as $chunk) { 159*37748cd8SNickeau if (!$this->validate($chunk)) { 160*37748cd8SNickeau return false; 161*37748cd8SNickeau } 162*37748cd8SNickeau } 163*37748cd8SNickeau return true; 164*37748cd8SNickeau } 165*37748cd8SNickeau 166*37748cd8SNickeau // If you end in W, make sure that it has a numeric in front of it 167*37748cd8SNickeau if ((bool) preg_match('/^\d{1,2}W$/', $value)) { 168*37748cd8SNickeau return true; 169*37748cd8SNickeau } 170*37748cd8SNickeau 171*37748cd8SNickeau return false; 172*37748cd8SNickeau } 173*37748cd8SNickeau} 174