Только для читателей Lifeexample возможно открыть интернет-магазин на Moguta.CMS со скидкой в 15%
Интеграция PayPal, простой PHP класс
Здравствуйте, уважаемые читатели блога LifeExample, в данной статье речь пойдет об интеграции мировой платежной системы PayPal с сайтом работающим на PHP. Этой осенью PayPal сообщили о начале работы с рублевыми переводами, а это значит что Россияне теперь смогут пользоваться самой надежной в мире системой электронных оплат.
PayPal (англ. «приятель, помогающий расплатиться») — крупнейший оператор электронных денежных средств. Позволяет клиентам оплачивать счета и покупки, отправлять и принимать денежные переводы. С октября 2002 года является подразделением компании eBay. По состоянию на 2012 год PayPal работает в 193 странах (хотя не во всех предоставляется полный набор услуг), имеет более 137 млн зарегистрированных пользователей, работает с 26 национальными валютами.
Просмотрев некоторые примеры в сети, я был поражен их несостоятельностью. Они все казались основанными на примере кода PayPal, который далек от совершенства. Кроме этого они в большинстве своем написаны процедурным стилем программирования, поэтому очень трудно понять все необходимые шаги. Но благодаря этим примерам у меня была возможность понять структуру, поэтому представленный в этой статье код является результатом моих находок. К счастью другие помогли мне решить те проблемы, с которыми я столкнулся.
Представленные ниже классы для интеграции PayPal на сайт, не являются сложными, но соответствуют моему требованию о покупке товара поштучно. Код не идеален, но вы всегда можете доработать его самостоятельно.
Я решил подойти к вопросу обработки IPN разбив процессы на объекты, отвечающие за выполнение логических под-задач для того, чтобы было понятно программистам знакомым с объектно-ориентированным программирование. ( Даже если вы не знакомы, я надеюсь, что моя статья даст вам возможность увидеть преимущества и лучше понять методологию, сэкономив много времени на будущее.)
Интеграция PayPal
Для того чтобы не упустить важные детали собственной настройки ниже я собираюсь копировать вставлять свой код и изменять соответствующую информацию. Конечно, статья из-за этого увеличится и будет создаваться впечатление сборки кода, но я считаю, что в самом коде содержится необходимая информация, и заранее приношу свои извинения, поскольку вам придется скопировать несколько файлов из этой статьи, если вы их собираетесь использовать для своего проекта и вставить их в собственный проект.
Мое решение разделяет процесс на пять объектов:
- payPalController — этот класс занимается обработкой логики не связанных с IPN частей PayPal процессов. Они занимается проверкой двойных транзакций, отправляемых PayPal, и проверяет факт совершения IPN транзакции именно по продукту с моего сайта. В таком случае, покупка добавляется в базу данных, а покупатель получает email.
- payPalIpn – место проведения обработки IPN. Очень важно!
- rmwStoreDB – с помощью этого класса транзакции добавляются в базу данных с моей стороны.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | <!doctype html public '-//W3C//DTD HTML 4.01//EN' 'http://www.w3.org/TR/html4/strict.dtd'> <html> <head> <title>Some Title</title> <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1"> <meta name="Generator" content="Alleycode HTML Editor"> <meta name="Description" content="Working Code"> <meta name="Keywords" content="Working Code"> </head> <body> <?php require_once ("../phpPayPalController.php"); $ppc = new payPalController(); $ppc->setTesting(false); $ppc->setLogging(false); $ppc->processPayPalIpnPayment(); ?> </body> |
Как вы видите payPalController — единственный класс, о котором нужно знать странице. Контроллер настроит остальные классы самостоятельно:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 | <?php require_once("phpPayPalIpnClass.php"); require_once("phpLoggerClass.php"); require_once("phpRmwStoreDbActions.php"); require_once("phpMyMailClass.php"); class payPalController { private $db; private $logger; private $testing; function __construct() { $this->logger = new myLogger(); $this->logger->setLogFile("logger.txt"); $this->logger->setLogging(false); $this->db = new rmwStoreDB(); $this->db->setLogger($this->logger); $this->testing = false; } function setTesting($state) { $this->testing = $state; $this->logger->setLogging($state); } function setLogging($state) { $this->logger->setLogging($state); } function processPayPalIpnPayment() { $processor = new payPalIpn(); $processor->setTesting($this->testing); $processor->setLogger($this->logger); $this->logger->log("Processing a payment.\r\n"); if (!$processor->processPost()) return; if ($this->duplicateTransaction()) return; if (!$this->verify()) return; if (!$this->addOrderToDatabase()) return; $this->sendProduct(); } private function duplicateTransaction() { $ret = false; if ($this->db->itemExists("orders", "payPalTransId", $_POST['txn_id'])) { $this->logger->log("Transaction: " . $_POST['txn_id'] . " exists\r\n"); $ret = true; } else { $this->logger->log("Transaction: " . $_POST['txn_id'] . " does not exist\r\n"); } return $ret; } private function verify() { if (!$this->db->itemExists("products", "id", $_POST ['item_number'])) { $this->logger->log("Item number: " . $_POST['item_number'] . " doesn't exist in database\r\n"); return false; } else { $this->logger->log("Item number: " . $_POST['item_number'] . " exists in database\r\n"); } $this->dbPrice = $this->db->getCellValue("price", "products", "id", $_POST['item_number']); if ($_POST['mc_gross'] < $this->dbPrice) { $this->logger->log("Payment received (" . $_POST ['mc_gross'] . ") less than item price. (" . $this->dbPrice . "\r\n"); return false; } else { $this->logger->log("Adequate payment received (" . $_POST ['mc_gross'] . ").\r\n"); } if ($_POST['mc_currency'] != "USD") { $this->logger->log("Paid in non-US funds - need to investigate.\r\n"); return false; } else { $this->logger->log("US Currency received - OK.\r\n"); } if ($_POST['receiver_email'] != "emailAddress@someplace.com" && $_POST['receiver_email'] != "sandboxEmailAddress@someplace.com") { $this->logger->log("Incorrect receiver email received (" . $_POST['receiver_email'] . ")\r\n"); return false; } else { $this->logger->log("Correct email received ( " . $_POST['receiver_email'] . ")\r\n"); } if ($_POST['payment_status'] != "Completed") { $this->logger->log("Payment incomplete from PayPal\r\n"); return false; } return true; } private function addOrderToDatabase() { $this->logger->log("Updating database.\r\n"); $this->db->addOrUpdateUser(); $this->db->addOrder(); return true; } private function sendProduct() { $mailHandler = new myMailer(); $mailHandler->setLogger($this->logger); if ($this->testing) { $mailTo = 'emailAddress@someplace.com'; } else { $mailTo = $_POST['payer_email']; } if ($_POST['item_number'] == "XXXX" || $_POST ['item_number'] == "XXXY") { if ($_POST['option_selection1'] == 'EPub') { $this->logger->log("Sending EPub to " . $mailTo . "\r\n"); $mailHandler->sendEbook('EPub', $mailTo); } else if ($_POST['option_selection1'] == 'MOBI') { $this->logger->log("Sending MOBI to " . $mailTo . "\r\n"); $mailHandler->sendEbook('MOBI', $mailTo); } else { $this->logger->log("SOMETHING WRONG - Not EPub or MOBI!\r\n"); } } } } ?> |
Теперь перейдем к классу payPalIpn. Как было сказано ранее, этот класс ответственен за обработку IPN и отправку корректных ответов на PayPal.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | <?php require_once("phpLoggerClass.php"); class payPalIpn { private $logger; private $ipnVerifiedC; private $testingC; function __construct() { $this->ipnVerifiedC = false; $this->testingC = false; } function ipnVerified() { return $this->ipnVerifiedC; } function setTesting($state) { $this->testingC = $state; } function setLogger(myLogger &$logFile) { $this->logger = $logFile; } function processPost() { $response = 'cmd=_notify-validate'; $magicQuotesFuncExists = false; if(function_exists('get_magic_quotes_gpc')) { $magicQuotesFuncExists = true; } $numPosts = 0; foreach ($_POST as $key => $value) { $numPosts += 1; if($magicQuotesFuncExists == true && get_magic_quotes_gpc() == 1){ $value = urlencode(stripslashes($value)); } else { $value = urlencode($value); } $response .= "&$key=$value"; } $header = "POST /cgi-bin/webscr HTTP/1.0\r\n"; $header .= "Host: www.sandbox.paypal.com\r\n"; $header .= "Content-Type: application/x-www-form-urlencoded\r\n"; $header .= "Content-Length: " . strlen($response) . "\r\n\r\n"; if ($this->testingC) { $socket = fsockopen ('ssl://www.sandbox.paypal.com', 443, $socketErrNum, $socketErrStr, 30); } else { $socket = fsockopen ('ssl://www.paypal.com', 443, $socketErrNum, $socketErrStr, 30); } //Oldie: $socket = fsockopen ('www.paypal.com', 80, $socketErrNum, //$socketErrStr, 30); if (!$socket) { $mail_Body = "Error from fsockopen:\r\n" . $socketErrStr . "\r\n\r\n" . "Original PayPal Post Data (COULD BE BOGUS!)" . "\r\n\r\n"; foreach ($_POST as $key => $value) { $value = urlencode(stripslashes($value)); $mail_Body .= "&$key=$value" . "\r\n"; } mail($myEmail, "IPN Error Noficiation: Failed to connect to PayPal", $mail_Body, "someone@somewhere.com"); $this->logger->log("Socket error: " . $socketErrStr); return; } $receivedVerification = false; if ($numPosts > 3) { fputs ($socket, $header . $response); $this->logger->log("\r\nSENT:\r\n" . $header . $response . "\r\n\r\n"); $receivedVerification = false; while (!feof($socket)) { $result = fgets ($socket, 1024); //Get a line of response $this->logger->log("RECEIVED: " . $result ."\r\n"); if (strcmp ($result, "VERIFIED") == 0) $receivedVerification = true; } } fclose ($socket); $ret = false; if ($receivedVerification == false) { $this->logger->log( "\r\n\r\nINVALID TRANSACTION! (Improper PayPal response received)\r\n"); } else { $this->ipnVerifiedC = true; $this->logger->log("TRANSACTION VERIFIED!\r\n"); $ret = true; } return $ret; } } ?> |
Следующее касается обработки баз данных. В файле db_config.php в первой строке хранится база данных с информацией о пользователях и о паролях. Очень важно, чтобы db_config.php не был видимым остальным, поэтому вам необходимо расположить его под самой видимой папкой вашего сайта.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 | require_once("db_config.php"); require_once("phpLoggerClass.php"); class rmwStoreDB { private $loggerC; private $lastRowC; function setlogger(mylogger $logFile) { $this->loggerC = $logFile; } function tellDb($sql) { $ret = mysql_query($sql); if (!$ret) { $this->loggerC->log( "DATABASE ERROR:\r\n" . mysql_error() . "\r\n" . "Query: " . HtmlEntities($sql)); die(); } return $ret; } function itemExists($table, $column, $value) { $rows = $this->tellDb("Select * from " . $table . " where " . $column . " = '" . $value . "'"); $lastRowC = mysql_fetch_array($rows); if ($lastRowC) return true; return false; } function getCellValue($what, $table, $column, $theId) { $rows = $this->tellDb("Select " . $what . " from " . $table . " where " . $column . " = " . $theId); $row = mysql_fetch_array($rows); if ($row) return $row['price']; else return 0.00; } function addOrUpdateUser() { $rows = $this->tellDb("Select * from customers where email = '" . $_POST['payer_email'] . "'"); $row = mysql_fetch_array($rows); if (!$row) { $this->loggerC->log("Adding user to database"); $this->addUser(); } else { $this->loggerC->log("User already exists in DB.\r\n"); $this->updateUser($row); } } private function addUser() { $cmd = "Insert into customers (firstName, lastName, shippingName, email, " . "addressLine1, city, state, zipCode, country) values ('" . $_POST['first_name'] . "', '" . $_POST['last_name'] . "', '" . $_POST['address_name'] . "', '" . $_POST['payer_email'] . "', '" . $_POST['address_street'] . "', '" . $_POST['address_city'] . "', '" . $_POST['address_state'] . "', '" . $_POST['address_zip'] . "', '" . $_POST['address_country'] . "')"; $this->tellDb($cmd); $this->loggerC->log("Added: '" . $_POST['first_name'] . "', '" . $_POST['last_name'] . "', '" . $_POST['address_name'] . "', '" . $_POST['payer_email'] . "', '" . $_POST['address_street'] . "', '" . $_POST['address_city'] . "', '" . $_POST['address_state'] . "', '" . $_POST['address_zip'] . "', '" . $_POST['address_country'] . "')"); } private function updateUser(array $row) { if ($row['firstName'] != $_POST['first_name'] || $row['lastName'] != $_POST['last_name'] || $row['shippingName'] != $_POST['address_name'] || $row['email'] != $_POST['payer_email'] || $row['addressLine1'] != $_POST['address_street'] || $row['city'] != $_POST['address_city'] || $row['state'] != $_POST['address_state'] || $row['zipCode'] != $_POST['address_zip'] || $row['country'] != $_POST['address_country']) { $cmd = "UPDATE customers SET "; $cmd .= "firstName = '" . $_POST['first_name'] . "', "; $cmd .= "lastName = '" . $_POST['last_name'] . "', "; $cmd .= "shippingName = '" . $_POST['address_name'] . "', "; $cmd .= "addressLine1 = '" . $_POST['address_street'] . "', "; $cmd .= "city = '" . $_POST['address_city'] . "', "; $cmd .= "state = '" . $_POST['address_state'] . "', "; $cmd .= "zipCode = '" . $_POST['address_zip'] . "', "; $cmd .= "country = '" . $_POST['address_country'] . "' "; $cmd .= "WHERE email = '" . $_POST['payer_email'] . "'"; $this->loggerC->log("\r\nChanging user with email " . $_POST['payer_email'] . "\r\n"); $old = $row['firstName'] . ", " . $row['lastName'] . ", " . $row['shippingName'] . ", " . $row['email'] . ", " . $row['addressLine1'] . ", " . $row['city'] . ", " .$row['state'] . ", " . $row['zipCode'] . ", " . $row['country'] . "\r\n\r\n"; $this->loggerC->log($old); $this->loggerC->log($cmd . "\r\n"); $this->tellDb($cmd); } } function addOrder() { $cmd = "Select id from customers where email = '" . $_POST['payer_email'] . "'"; $rows = $this->tellDb($cmd); $row = mysql_fetch_array($rows); if (!$row) { $this->loggerC->log("HUGE PROBLEM! CUSTOMER ID NOT FOUND -" . " ABORTING\r\n"); die(); } $id = $row['id']; $theDate = date('F j, Y, g:i a'); $tz = date('T'); $ppID = $_POST['txn_id']; $grossPay = $_POST['payment_gross']; $shipping = $_POST['shipping']; $cmd = "Insert into orders (customer, date, timeZone, payPalTransId, " . "grossPmt, shipping) values ('$id', '" . "$theDate', '$tz', '$ppID', '" . "$grossPay', '$shipping')"; $this->tellDb($cmd); $this->loggerC->log("Inserting order into orders table:\r\n" . $cmd . "\r\n\r\n"); $cmd = "Select id from orders where payPalTransId = '$ppID'"; $rows = $this->tellDb($cmd); $row = mysql_fetch_array($rows); if (!$row) { $this->loggerC->log("HUGE PROBLEM! ORDER ID NOT FOUND -" . " ABORTING\r\n"); die(); } $id = $row['id']; $itemNum = $_POST['item_number']; $qty = $_POST['quantity']; $info = $_POST['option_selection1']; $cmd = "Insert into orderItems (orderNumber, item, quantity, extraInfo)" . " values('$id', '$itemNum', '$qty'," . " '$info')"; $this->loggerC->log( "Inserting into order items:\r\n" . $cmd . "\r\n"); $this->tellDb($cmd); } } ?> |
Ура! Следующие классы PHP для интеграции PayPal намного меньше предыдущих! Первый класс — почтового клиента. Понятное дело, что при его простоте можно сказать, что он должен быть более ярким и отправлять получше чем сейчас отформатированные HTML ответы на покупки. Но всему свое время.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | <?php require_once("emailPassword.php"); require_once("phpLoggerClass.php"); require_once("/path_to/swift_required.php"); class myMailer { private $myEmail; private $logger; function __construct() { $this->myEmail = "someone@somewhere.com"; } function setLogger(myLogger &$logFile) { $this->logger = $logFile; } function sendEbook($ebookType, $mailTo) { $this->mailWithAttachment($fileName, $path, $mailTo, $this->myEmail, $from, $replyTo, $subject, $msg); } private function mailWithAttachment($filename, $path, $mailTo, $from_mail, $from_name, $replyto, $subject, $message) { $transport = Swift_SmtpTransport::newInstance('mailServer.com', 465, 'ssl') ->setUsername(hiddenEmailAccount()) ->setPassword(hiddenEmailPassword()) ; $mailer = Swift_Mailer::newInstance($transport); $message = Swift_Message::newInstance() ->setSubject($subject) ->setFrom(array($from_mail => $from_name)) ->setTo(array($mailTo)) ->setBody($message) //->addPart('<p>Here is the message itself</p>', 'text/html') ->attach(Swift_Attachment::fromPath($path.$filename)) ; $this->logger->forceLog("Sending " . $filename . " to " . $mailTo . " : "); $result = $mailer->send($message); $this->logger->forceLog("Result = " . $result . "\r\n"); } } ?> |
Последним идет класс логов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <?php class myLogger { private $fileNameC; private $doLoggingC; function __construct() { $this->fileNameC = "log.txt"; $this->doLoggingC = true; } function setLogFile($fileName) { $this->fileNameC = $fileName; } function setLogging($state) { $this->doLoggingC = $state; } function log($msg) { if ($this->doLoggingC == true) { file_put_contents($this->fileNameC, $msg, FILE_APPEND); } } function forceLog($msg) { file_put_contents($this->fileNameC, $msg, FILE_APPEND); } } ?> |
Также необходимы еще два файла, если хотите, можете их скомбинировать в один, внеся представленные изменения. Они предназначены для хранения информации базы данных и информации о email. Повторюсь еще раз, очень важно, чтобы эти файлы не находились в широком доступе, поэтому рекомендую ограничить к ним доступ.
Содержание db_config.php файла следующее. За исключением того, что здесь не представлены реальные пользователи, пароли и базы данных!
1 2 | $db_con = mysql_connect("localhost", "theUser", "thePassword", true) or die(mysql_error()); $db_selected = mysql_select_db("theDatabase") or die(mysql_error()); |
И наконец, файл с паролем email («emailPassword.php» в представленном выше коде):
1 2 3 4 5 6 7 8 9 10 11 | <?php function hiddenEmailAccount() { return "someone@somewhere.com"; } function hiddenEmailPassword() { return "thePassword"; } ?> |
Надеюсь, представленная информация была полезной, если вы собираетесь заниматься интеграцией PayPal с помощью простого PHP класса. Спасибо за того, что прочитали мою статью и удачного вам программирования. Если вам удастся, что-то улучшить, буду рад увидеть улучшения в комментариях.
Читайте также похожие статьи:
Чтобы не пропустить публикацию следующей статьи подписывайтесь на рассылку по E-mail или RSS ленту блога.
Комментарии
/path_to/swift_required.php это что такое?
библиотека для работы с письмами swiftmailer.org
Добрый день!
Хочу использовать Ваш класс у себя на сайте — как его правильно встроить в сайт — можно пример реальной странички для оплаты. Заранее благодарен.
Мощно для 2013 года, эталонный говнокод. Ну хоть оформление норм.
Данный класс не рекомендуется к использованию в продакшене, но для ознакомления сойдет.
Именно для ознакомления и предназначены все статьи и листинги данного ресурса.
а что ты посоветуешь?