Validation Credit Card - Using Symfony

Validation in symfony is pretty easy. Butttt, sometimes we need to create some “custom” function. Let’s talk about it today.

I’m working at a lovelly project. My client give me freedom to develop. That’s good, but a big responsability too. I’m working hard to do the best that I can do. A good development, object oriented, and when I see something that I would do better I go back and do again, better!

That’s the way with credit card validation. I get a excelent class created by John Garden, on the far away year of 2005. I customized it, changed somethings, and I’m using it with symfony. Automagically works good, and it’s in portugues and english.

First, at SomethingForm.class.php I do two little things

At function configure(), I put

// add a post validator
$this->validatorSchema->setPostValidator(
new sfValidatorCallback(array('callback' => array($this, 'checkCreditCard')))
);

And above, the function checkCreditCard():

    public function checkCreditCard($validator, $values)
    {
        if ($values['numero'])
        {
            $oCreditCard = new myCreditCardValidator($values);
            $sError = $oCreditCard->execute($values['tipo'], $values['numero'], ‘pt_BR’);

            if ($sError != 1){
                // Create the error object
                $sfError = new sfValidatorError($validator, $sError);
                // throw an error bound to the number field
                throw new sfValidatorErrorSchema($validator, array(’numero’ => $sfError));
            }
        }
        return $values;
    }

The class, is above.

<?php

/**
 * This class has been converted to a Symfony Validator from original code
 * created by John Gardner, 4th January 2005.
 * http://www.braemoor.co.uk/software/index.shtml
 *
 * Symfony conversion by Fellipe Brito, Lado Direito Solucoes, 2009
 * http://www.ladodireito.com
 *
 **/

class myCreditCardValidator
{
    static protected $CARDS = array (
    array ('name'           => 'American Express',
    'length'            => '15',
    'prefixes'      => '34,37',
    'checkdigit'    => true
    ),
    array ('name'           => 'Carte Blanche',
    'length'         => '14',
    'prefixes'   => '300,301,302,303,304,305,36,38',
    'checkdigit' => true
    ),
    array ('name'       => 'Diners Club',
    'length'         => '14',
    'prefixes'   => '300,301,302,303,304,305,36,38',
    'checkdigit' => true
    ),
    array ('name'       => 'Discover',
    'length'         => '16',
    'prefixes'   => '6011',
    'checkdigit' => true
    ),
    array ('name'       => 'Enroute',
    'length'         => '15',
    'prefixes'   => '2014,2149',
    'checkdigit' => true
    ),
    array ('name'       => 'JCB',
    'length'         => '15,16',
    'prefixes'   => '3,1800,2131',
    'checkdigit' => true
    ),
    array ('name'       => 'Maestro',
    'length'         => '16',
    'prefixes'   => '5020,6',
    'checkdigit' => true
    ),
    array ('name'       => 'MasterCard',
    'length'         => '16',
    'prefixes'   => '51,52,53,54,55',
    'checkdigit' => true
    ),
    array ('name'       => 'Solo',
    'length'         => '16,18,19',
    'prefixes'   => '6334, 6767',
    'checkdigit' => true
    ),
    array ('name'       => 'Switch',
    'length'         => '16,18,19',
    'prefixes'   => '4903,4905,4911,4936,564182,633110,6333,6759',
    'checkdigit' => true
    ),
    array ('name'       => 'Visa',
    'length'         => '13,16',
    'prefixes'   => '4',
    'checkdigit' => true
    ),
    array ('name'       => 'Visa Electron',
    'length'         => '16',
    'prefixes'   => '417500,4917,4913',
    'checkdigit' => true
    )
    );

    static protected $ERRORS = array(
    "en" => array(
    "cc_error_type" => 'Unknown card type',
    "cc_error_missing" => 'No card number provided',
    "cc_error_format" => 'Credit card number has invalid format',
    "cc_error_number" => 'Credit card number is invalid',
    "cc_error_length" => 'Credit card number is wrong length'
    ),
    "pt_BR" => array(
    "cc_error_type" => 'Tipo de cartão desconhecido',
    "cc_error_missing" => 'Nenhum número de cartão informado',
    "cc_error_format" => 'Este número tem um formato inválido',
    "cc_error_number" => 'Este número é inválido',
    "cc_error_length" => 'Este número não tem um tamanho correto'
    )
    );

    public function execute(&$cardName, &$cardNumber, $sCulture)
    {
        // Establish card type
        $cardType = -1;
        for ($i=0; $i<sizeof(self::$CARDS); $i++)
        {   // See if it is this card (ignoring the case of the string)
            if (strtolower($cardName) == strtolower(self::$CARDS[$i]['name']))
            {
                $cardType = $i;
                break;
            }
        }

        // If card type not found, report an error
        if ($cardType == -1)
        {
            $error = self::$ERRORS[$sCulture]['cc_error_type'];
            return $error;
        }

        // Ensure that the user has provided a credit card number
        if (strlen($cardNumber) == 0)
        {
            $error = self::$ERRORS[$sCulture]['cc_error_missing'];
            return $error;
        }

        // Remove any non-digits   from the credit card number
        $cardNo = preg_replace(’/[^0-9]/’, ”, $cardNumber);

        // Check that the number is numeric and of the right sort of length.
        if (!eregi(’^[0-9]{13,19}$’,$cardNo))
        {
            $error = self::$ERRORS[$sCulture]['cc_error_format'];
            return $error;
        }

        // Now check the modulus 10 check digit - if required
        if (self::$CARDS[$cardType]['checkdigit'])
        {
            $checksum = 0;   // running checksum total
            $mychar = “”;    // next char to process
            $j = 1;          // takes value of 1 or 2

            // Process each digit one by one starting at the right
            for ($i = strlen($cardNo) - 1; $i >= 0; $i–)
            {
                // Extract the next digit and multiply by 1 or 2 on alternative digits.
                $calc = $cardNo{$i} * $j;

                // If the result is in two digits add 1 to the checksum total
                if ($calc > 9) {
                    $checksum = $checksum + 1;
                    $calc = $calc - 10;
                }

                // Add the units element to the checksum total
                $checksum = $checksum + $calc;

                // Switch the value of j
                if ($j ==1) {$j = 2;} else {$j = 1;};
            }

            // All done - if checksum is divisible by 10, it is a valid modulus 10.
            // If not, report an error.
            if ($checksum % 10 != 0)
            {
                $error = self::$ERRORS[$sCulture]['cc_error_number'];
                return $error;
            }
        }

        // The following are the card-specific checks we undertake.

        // Load an array with the valid prefixes for this card
        $prefix = split(’,',self::$CARDS[$cardType]['prefixes']);

        // Now see if any of them match what we have in the card number
        $prefixValid = false;
        for ($i=0; $i<sizeof($prefix); $i++)
        {
            $exp = ‘^’ . $prefix[$i];
            if (ereg($exp,$cardNo))
            {
                $prefixValid = true;
                break;
            }
        }

        // If it isn’t a valid prefix there’s no point at looking at the length
        if (!$prefixValid)
        {
            $error = self::$ERRORS[$sCulture]['cc_error_number'];
            return $error;
        }

        // See if the length is valid for this card
        $lengthValid = false;
        $lengths = split(’,',self::$CARDS[$cardType]['length']);
        for ($j=0; $j<sizeof($lengths); $j++)
        {
            if (strlen($cardNo) == $lengths[$j])
            {
                $lengthValid = true;
                break;
            }
        }

        // See if all is OK by seeing if the length was valid.
        if (!$lengthValid)
        {
            $error = self::$ERRORS[$sCulture]['cc_error_length'];
            return $error;
        };

        // The credit card is in the required format.
        return true;
    }
}

Respond to this post