File: YACronParser.php

Recommend this page to a friend!
  Classes of Brett Dutton  >  Yet Another PHP Cron Parser  >  YACronParser.php  >  Download  
File: YACronParser.php
Role: Class source
Content type: text/plain
Description: All inclusive Class plus the test cases at the end
Class: Yet Another PHP Cron Parser
Parse a cron string and get the last run time
Author: By
Last change: Minor bug fix - Incorrect class name in tests
Date: 4 years ago
Size: 13,319 bytes
 

Contents

Class file image Download
<?php
// namespace Jackbooted\Cron;

/**
 * @copyright Confidential and copyright (c) 2016 Jackbooted Software. All rights reserved.
 *
 * Written by Brett Dutton of Jackbooted Software
 * brett at brettdutton dot com
 *
 * This software is written and distributed under the GNU General Public
 * License which means that its source code is freely-distributed and
 * available to the general public.
 */

/*
 * There are a few Cron Parser classes (about 3)
 * All were way to big for what was needed. This is an attempt to simplify the process
 * Not the most optimised
 *
 * For this to be useful need to keep track of the last time that the command was run.
 * This means that you can use a construct like this
 *
 *      foreach ( self::getCronTabList () as $sheduleItem ) {
 *
 *          $storedLastRunTime = strtotime( ( $sheduleItem->lastRun == '' ) ? $sheduleItem->start : $sheduleItem->lastRun );
 *          $previousCalculatedRunTime = CronParser::lastRun( $sheduleItem->cron );
 *
 *          // This looks at when the item had run. If the stored value is less than
 *          // the calculated value means that we have past a run period. So need to run
 *          if ( $storedLastRunTime <  $previousCalculatedRunTime ) {
 *
 *              // Update the run time to now
 *              $sheduleItem->lastRun = strftime ( '%Y-%m-%d %H:%M', $previousCalculatedRunTime );
 *              $sheduleItem->save ();
 *
 *              // Do the work to run the cron job here
 *              // ..................
 *          }
 *      }
 *
 * Cron Definition
 # * * * * *
 # - - - - -
 # | | | | |
 # | | | | |
 # | | | | +----- day of week (0 - 6) (0 to 6 are Sunday to Saturday, or use names; 7 is Sunday, the same as 0)
 # | | | +---------- month (1 - 12)
 # | | +--------------- day of month (1 - 31)
 # | +-------------------- hour (0 - 23)
 # +------------------------- min (0 - 59)
 *
 * See the code at the end of this class to see some examples
 *
 */
class YACronParser {

    /**
     * Gets the last run date before now
     * @param String $cronString
     * @return number time in seconds that the last cron task ran
     */
    public static function lastRun ( $cronString ) {
        $originalString = $cronString;

        $mappings = [
            '@yearly'   => '0 0 1 1 *',
            '@annually' => '0 0 1 1 *',
            '@monthly'  => '0 0 1 * *',
            '@weekly'   => '0 0 * * 0',
            '@daily'    => '0 0 * * *',
            '@hourly'   => '0 * * * *'
        ];
        if ( isset ( $mappings[$cronString] ) ) {
            $cronString = $mappings[$cronString];
        }

        // Reduces white spaces
        $cronString = preg_replace( '/[\s]{2,}/', ' ', $cronString);

        // recognise the really simple case
        if ( $cronString == '* * * * *' ) {
            $calcTime = ( (int)( time() / 60 ) ) * 60;
            return $calcTime;
        }

        // Get rid of names
        $cronString = strtolower( $cronString );
        $cronString = str_replace (  [ 'sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday' ],
                                     [ '0',      '1',      '2',       '3',         '4',        '5',      '6' ],
                                    $cronString );

        // Get the parts if not 5 return every minute
        $cronParts = explode( ' ' , $cronString );
        if ( count ( $cronParts ) != 5 ) {
            $calcTime = ( (int)( time() / 60 ) ) * 60;
            return $calcTime;
        }

        // evaluate the valid ranges for all of the parts
        $cronParts[0] = self::cronPartToRange( $cronParts[0], range( 0, 59 ) ); // Minutes
        $cronParts[1] = self::cronPartToRange( $cronParts[1], range( 0, 23 ) ); // Hours
        $cronParts[2] = self::cronPartToRange( $cronParts[2], range( 1, 31 ) ); // Day of Month
        $cronParts[3] = self::cronPartToRange( $cronParts[3], range( 1, 12 ) ); // Month of the year
        $cronParts[4] = self::cronPartToRange( $cronParts[4], range( 0, 6 ) );  // Day of the week
        $cronParts[5] =  [ (int)date( 'Y' ) - 2, (int)date( 'Y' ) - 1, (int)date( 'Y' ) ]; // only go back 2 years

        // ******* TODO ****************
        // The code below is usually commented out.
        // Do not need this in production. So when you are done, remove
        echo '<br/>' . "\n" . 'Original: ' . $originalString . ' Optimised: ' . $cronString . '<br/>' . "\n";
        $cols = [ 'min', 'hrs', 'day or month', 'month', 'day of week', 'year'];
        foreach ( $cronParts as $idx => $part ) {
            echo $idx . ' - ' . $cols[$idx] . ' - [' . join( ', ', $part ) . ']<br/>' . "\n";
        }

        // Find the index for the last run based on current time
        $correctParts =  [ 0, 0, 0, 0, 0, 0 ];
        $numDaysOfWeek = count ( $cronParts[4] );
        $now = time();

        // search down the to the last run
        for ( $correctParts[5]=count( $cronParts[5] ) - 1; $correctParts[5]>=0; $correctParts[5]-- ) {

            for ( $correctParts[3]=count( $cronParts[3] ) - 1; $correctParts[3]>=0; $correctParts[3]-- ) {
                for ( $correctParts[2]=count( $cronParts[2] ) - 1; $correctParts[2]>=0; $correctParts[2]-- ) {

                    // If this is not a valid day then skip it
                    if ( $numDaysOfWeek != 7 && ! in_array ( self::getDayOfWeek( $cronParts, $correctParts ), $cronParts[4] ) ) {
                        continue;
                    }

                    // Calc hour and minute
                    for ( $correctParts[1]=count( $cronParts[1] ) - 1; $correctParts[1]>=0; $correctParts[1]-- ) {
                        for ( $correctParts[0]=count( $cronParts[0] ) - 1; $correctParts[0]>=0; $correctParts[0]-- ) {
                            $calcTime = self::calcTime( $cronParts, $correctParts );
                            if ( $calcTime < $now ) {
                                return $calcTime;
                            }
                        }
                    }
                }
            }
        }

        // Made it to here then return stanadrd time
        return ( (int)( time() / 60 ) ) * 60;
    }

    /**
     * Calculates the time based on the pointers into the valid range of array.
     *
     * @param array $cronParts array of arrays containing the valid ranges based on cron string
     * @param array $correctParts pointers into the array that we are testing
     * @return number The time based on the valid cron pieces
     */
    private static function calcTime ( $cronParts, $correctParts ) {
        $tim = mktime ( $cronParts[1][$correctParts[1]],
                        $cronParts[0][$correctParts[0]],
                        0,
                        $cronParts[3][$correctParts[3]],
                        $cronParts[2][$correctParts[2]],
                        $cronParts[5][$correctParts[5]] );
        //echo '$cronParts  - [' . join( ', ', $cronParts  ) . ']<br/>' . "\n";
        //echo '$correctParts  - [' . join( ', ', $correctParts  ) . ']<br/>' . "\n";
        //echo date ( 'Y-m-d H:i', $tim ) . '<br/>' . "\n";
        return $tim;
    }
    /**
     * Calculates the day of the week based on the ranges and pointers into the current
     * @param unknown $cronParts
     * @param unknown $correctParts
     * @return number day of week 0-6
     */
    private static function getDayOfWeek ( $cronParts, $correctParts ) {
        $tim = mktime ( 0,
                        0,
                        0,
                        $cronParts[3][$correctParts[3]],
                        $cronParts[2][$correctParts[2]],
                        $cronParts[5][$correctParts[5]] );
        return (int)date( 'w', $tim );
    }

    /**
     * Takes the cron component and the range of valid numbers and reduces the list.
     * This method calls itself recursively until all the elements are evaluated
     *
     * @param type $part the part of the cron string
     * @param type $fullRange. For the position the $fullRange variable contains all the elements that would be valid.
     *                         e.g. minutes would be range( 0, 59 )
     * @return type
     */
    private static function cronPartToRange ( $part, $fullRange ) {

        // simple digit so just return
        if ( preg_match ( '/^[0-9]+$/', $part ) ) {
            return  [ (int)$part ];
        }

        // If there are commas then multiple components.
        // get all the components and then merge, sort and uniq
        else if ( strpos( $part, ',' ) !== false ) {
            $validValues =  [];
            foreach ( explode( ',',  $part ) as $bit ) {
                $validValues = array_merge( $validValues, self::cronPartToRange( $bit, $fullRange ) );
            }
            $validValues = array_unique ( $validValues );
            sort ( $validValues );
            return $validValues;
        }

        // This is the starting point and the divisor e.g.
        // */5 = start at 0 and select every 5th element so 0, 5, 10 ....
        // or 2/3 = start at second element and then every 3rd element so 2, 5,
        // LHS could be a range like 30-50
        else if ( strpos( $part, '/' ) !== false ) {
            //Split to LHS and RHS
            $bits = explode( '/', $part );

            // recursivley get the range of LHS
            $lhsRange = self::cronPartToRange( $bits[0], $fullRange );

            // If there is no elements then some failure and should not happen default to all
            if ( count ( $lhsRange ) == 0 ) {
                $lhsRange = $fullRange;
            }

            // If this has one element then it must be starting point for the
            // so something like 5/15 => 5, 20, 35, 50
            // Take off the full range of elements till you get to the matching one
            else if ( count ( $lhsRange ) == 1 ) {
                while ( count ( $fullRange ) > 0 && $fullRange[0] != $lhsRange[0] ) {
                    array_shift( $fullRange );
                }
                $lhsRange = $fullRange;
            }

            $rhsElement = (int)$bits[1];

            // Identify the positions that match the RHS. Step through the array and see if the indexes
            // modulus rhs is 0
            $validValues =  [];
            foreach ( $lhsRange as $idx => $num ) {
                if ( ( $idx % $rhsElement ) == 0 ) $validValues[] = $num;
            }
            return $validValues;
        }

        // Must be a range, so split LHS and RHS to get the valid range
        else if ( strpos( $part, '-' ) !== false ) {
            $bits = explode( '-', $part );
            return range ( (int)$bits[0], (int)$bits[1] );
        }

        // Default to full range
        else {
            return $fullRange;
        }
    }
}
// ******* TODO ****************
// The code below is usually commented out.
// Do not need this in production. So when you are done, remove
date_default_timezone_set ( 'Australia/Brisbane' );
echo 'Simple case - no calcs - LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '* * * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '5/15 0 * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '0/15 * * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '0 2 * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '* 11-20 * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '0/15 * * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '* 12-21 * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '0 2 * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '1-59/2 14-23 * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '0-58/2 14-23 * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '* * * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '* 22-23 * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '1-2 22-23 * * Monday-Wednesday' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '1,2,3,4,10-20 11-20 * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '0 */4 * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '0 0/4 * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '0 2/3 * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '30-50/3 * * * *' ) ) . '<br/>' . "\n";
echo 'LastRun Date: ' . date ( 'Y-m-d H:i', YACronParser::lastRun( '0 0 31 * *' ) ) . '<br/>' . "\n";


For more information send a message to info at phpclasses dot org.