/src/CreditCard.php

https://github.com/inacho/php-credit-card-validator · PHP · 228 lines · 186 code · 28 blank · 14 comment · 27 complexity · acbd8f8aaa30d8ead9b560f2d2c10eed MD5 · raw file

  1. <?php
  2. /**
  3. * Validates popular debit and credit cards numbers against regular expressions and Luhn algorithm.
  4. * Also validates the CVC and the expiration date.
  5. *
  6. * @author Ignacio de Tomás <nacho@inacho.es>
  7. * @copyright 2014 Ignacio de Tomás (http://inacho.es)
  8. */
  9. namespace Inacho;
  10. class CreditCard
  11. {
  12. protected static $cards = array(
  13. // Debit cards must come first, since they have more specific patterns than their credit-card equivalents.
  14. 'visaelectron' => array(
  15. 'type' => 'visaelectron',
  16. 'pattern' => '/^4(026|17500|405|508|844|91[37])/',
  17. 'length' => array(16),
  18. 'cvcLength' => array(3),
  19. 'luhn' => true,
  20. ),
  21. 'maestro' => array(
  22. 'type' => 'maestro',
  23. 'pattern' => '/^(5(018|0[23]|[68])|6(39|7))/',
  24. 'length' => array(12, 13, 14, 15, 16, 17, 18, 19),
  25. 'cvcLength' => array(3),
  26. 'luhn' => true,
  27. ),
  28. 'forbrugsforeningen' => array(
  29. 'type' => 'forbrugsforeningen',
  30. 'pattern' => '/^600/',
  31. 'length' => array(16),
  32. 'cvcLength' => array(3),
  33. 'luhn' => true,
  34. ),
  35. 'dankort' => array(
  36. 'type' => 'dankort',
  37. 'pattern' => '/^5019/',
  38. 'length' => array(16),
  39. 'cvcLength' => array(3),
  40. 'luhn' => true,
  41. ),
  42. // Credit cards
  43. 'visa' => array(
  44. 'type' => 'visa',
  45. 'pattern' => '/^4/',
  46. 'length' => array(13, 16),
  47. 'cvcLength' => array(3),
  48. 'luhn' => true,
  49. ),
  50. 'mastercard' => array(
  51. 'type' => 'mastercard',
  52. 'pattern' => '/^(5[0-5]|2[2-7])/',
  53. 'length' => array(16),
  54. 'cvcLength' => array(3),
  55. 'luhn' => true,
  56. ),
  57. 'amex' => array(
  58. 'type' => 'amex',
  59. 'pattern' => '/^3[47]/',
  60. 'format' => '/(\d{1,4})(\d{1,6})?(\d{1,5})?/',
  61. 'length' => array(15),
  62. 'cvcLength' => array(3, 4),
  63. 'luhn' => true,
  64. ),
  65. 'dinersclub' => array(
  66. 'type' => 'dinersclub',
  67. 'pattern' => '/^3[0689]/',
  68. 'length' => array(14),
  69. 'cvcLength' => array(3),
  70. 'luhn' => true,
  71. ),
  72. 'discover' => array(
  73. 'type' => 'discover',
  74. 'pattern' => '/^6([045]|22)/',
  75. 'length' => array(16),
  76. 'cvcLength' => array(3),
  77. 'luhn' => true,
  78. ),
  79. 'unionpay' => array(
  80. 'type' => 'unionpay',
  81. 'pattern' => '/^(62|88)/',
  82. 'length' => array(16, 17, 18, 19),
  83. 'cvcLength' => array(3),
  84. 'luhn' => false,
  85. ),
  86. 'jcb' => array(
  87. 'type' => 'jcb',
  88. 'pattern' => '/^35/',
  89. 'length' => array(16),
  90. 'cvcLength' => array(3),
  91. 'luhn' => true,
  92. ),
  93. );
  94. public static function validCreditCard($number, $type = null)
  95. {
  96. $ret = array(
  97. 'valid' => false,
  98. 'number' => '',
  99. 'type' => '',
  100. );
  101. // Strip non-numeric characters
  102. $number = preg_replace('/[^0-9]/', '', $number);
  103. if (empty($type)) {
  104. $type = self::creditCardType($number);
  105. }
  106. if (array_key_exists($type, self::$cards) && self::validCard($number, $type)) {
  107. return array(
  108. 'valid' => true,
  109. 'number' => $number,
  110. 'type' => $type,
  111. );
  112. }
  113. return $ret;
  114. }
  115. public static function validCvc($cvc, $type)
  116. {
  117. return (ctype_digit($cvc) && array_key_exists($type, self::$cards) && self::validCvcLength($cvc, $type));
  118. }
  119. public static function validDate($year, $month)
  120. {
  121. $month = str_pad($month, 2, '0', STR_PAD_LEFT);
  122. if (! preg_match('/^20\d\d$/', $year)) {
  123. return false;
  124. }
  125. if (! preg_match('/^(0[1-9]|1[0-2])$/', $month)) {
  126. return false;
  127. }
  128. // past date
  129. if ($year < date('Y') || $year == date('Y') && $month < date('m')) {
  130. return false;
  131. }
  132. return true;
  133. }
  134. // PROTECTED
  135. // ---------------------------------------------------------
  136. protected static function creditCardType($number)
  137. {
  138. foreach (self::$cards as $type => $card) {
  139. if (preg_match($card['pattern'], $number)) {
  140. return $type;
  141. }
  142. }
  143. return '';
  144. }
  145. protected static function validCard($number, $type)
  146. {
  147. return (self::validPattern($number, $type) && self::validLength($number, $type) && self::validLuhn($number, $type));
  148. }
  149. protected static function validPattern($number, $type)
  150. {
  151. return preg_match(self::$cards[$type]['pattern'], $number);
  152. }
  153. protected static function validLength($number, $type)
  154. {
  155. foreach (self::$cards[$type]['length'] as $length) {
  156. if (strlen($number) == $length) {
  157. return true;
  158. }
  159. }
  160. return false;
  161. }
  162. protected static function validCvcLength($cvc, $type)
  163. {
  164. foreach (self::$cards[$type]['cvcLength'] as $length) {
  165. if (strlen($cvc) == $length) {
  166. return true;
  167. }
  168. }
  169. return false;
  170. }
  171. protected static function validLuhn($number, $type)
  172. {
  173. if (! self::$cards[$type]['luhn']) {
  174. return true;
  175. } else {
  176. return self::luhnCheck($number);
  177. }
  178. }
  179. protected static function luhnCheck($number)
  180. {
  181. $checksum = 0;
  182. for ($i=(2-(strlen($number) % 2)); $i<=strlen($number); $i+=2) {
  183. $checksum += (int) ($number{$i-1});
  184. }
  185. // Analyze odd digits in even length strings or even digits in odd length strings.
  186. for ($i=(strlen($number)% 2) + 1; $i<strlen($number); $i+=2) {
  187. $digit = (int) ($number{$i-1}) * 2;
  188. if ($digit < 10) {
  189. $checksum += $digit;
  190. } else {
  191. $checksum += ($digit-9);
  192. }
  193. }
  194. if (($checksum % 10) == 0) {
  195. return true;
  196. } else {
  197. return false;
  198. }
  199. }
  200. }