This repository has been archived by the owner on Apr 19, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 548
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add PHP implementation of the server * Fixed composer start script * Using Slim built-in method for raw body in webhooks * Minor fixes to README.md * Fixed settings: currency, country and API version * Fixed currency key name in routes * Moved settings to ini file
- Loading branch information
1 parent
69521f4
commit 37d278f
Showing
9 changed files
with
532 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
/vendor/ | ||
/logs/* | ||
composer.lock | ||
settings.ini |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
<IfModule mod_rewrite.c> | ||
RewriteEngine On | ||
|
||
# Some hosts may require you to use the `RewriteBase` directive. | ||
# Determine the RewriteBase automatically and set it as environment variable. | ||
# If you are using Apache aliases to do mass virtual hosting or installed the | ||
# project in a subdirectory, the base path will be prepended to allow proper | ||
# resolution of the index.php file and to redirect to the correct URI. It will | ||
# work in environments without path prefix as well, providing a safe, one-size | ||
# fits all solution. But as you do not need it in this case, you can comment | ||
# the following 2 lines to eliminate the overhead. | ||
RewriteCond %{REQUEST_URI}::$1 ^(/.+)/(.*)::\2$ | ||
RewriteRule ^(.*) - [E=BASE:%1] | ||
|
||
# If the above doesn't work you might need to set the `RewriteBase` directive manually, it should be the | ||
# absolute physical path to the directory that contains this htaccess file. | ||
# RewriteBase / | ||
|
||
# RewriteCond %{REQUEST_FILENAME} !-f | ||
RewriteRule ^ index.php [QSA,L] | ||
</IfModule> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
# Stripe Payments Demo — PHP Server | ||
|
||
This demo uses: | ||
- [Slim](http://www.slimframework.com/) as the API framework. | ||
- [stripe-php](https://github.com/stripe/stripe-php) as the SDK to interact with Stripe's APIs | ||
- [monolog/monolog](https://github.com/Seldaek/monolog) as the logging interface | ||
|
||
## Payments Integration | ||
|
||
- [`settings.php`](settings.php) contains the Stripe account configuration as well as the payment methods accepted in this demo. | ||
- [`index.php`](index.php) contains the routes that interface with Stripe to create PaymentIntents and receive webhook events. | ||
- [`store`](store) contains inventory and shipping utils. | ||
|
||
## Requirements | ||
|
||
You’ll need the following: | ||
|
||
- PHP 5.4 or higher | ||
- Modern browser that supports ES6 (Chrome to see the Payment Request, and Safari to see Apple Pay). | ||
- Stripe account to accept payments ([sign up](https://dashboard.stripe.com/register) for free!) | ||
|
||
## Getting Started | ||
|
||
After cloning the repository, you first have to install the dependencies using composer: | ||
|
||
``` | ||
cd server/php | ||
composer install | ||
``` | ||
|
||
Rename `settings.ini.example` to `settings.ini` and update your [Stripe API keys](https://dashboard.stripe.com/account/apikeys) and any other configuration details you might want to add. | ||
|
||
That's it, you can now run the application by using the PHP built-in server still from the `server/php` directory: | ||
|
||
``` | ||
composer start | ||
``` | ||
|
||
You should now see it running on [`http://localhost:8888/`](http://localhost:8888/) | ||
|
||
Optionally you can change the directory tree to fit your needs, don't forget to: | ||
- change the path to the `public` directory in `settings.php` | ||
- uncomment the `RewriteBase` option in [`.htaccess`](.htaccess) and update the value accordingly if needed | ||
|
||
### Testing Webhooks | ||
|
||
If you want to test [receiving webhooks](https://stripe.com/docs/webhooks), we recommend using ngrok to expose your local server. | ||
|
||
First [download ngrok](https://ngrok.com) and start your PHP application. | ||
|
||
[Run ngrok](https://ngrok.com/docs). Assuming your PHP application is running on the port 8888, you can simply run ngrok in your Terminal in the directory where you downloaded ngrok: | ||
|
||
``` | ||
ngrok http 8888 | ||
``` | ||
|
||
ngrok will display a UI in your terminal telling you the new forwarding address for your PHP app. Use this URL as the URL to be called in your developer [webhooks panel.](https://dashboard.stripe.com/account/webhooks) | ||
|
||
Don't forget to append `/webhook` when you set up your Stripe webhook URL in the Dashboard. Example URL to be called: `http://75795038.ngrok.io/webhook`. | ||
|
||
## Logging | ||
|
||
The application logs webhook events in `server/php/logs`. Make sure your server has write access to that directory. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
{ | ||
"name": "stripe/stripe-payments-demo-php", | ||
"description": "A PHP backend for the Stripe Payments demo shop", | ||
"keywords": ["stripe", "payments", "demo"], | ||
"homepage": "https://github.com/stripe/stripe-payments-demo", | ||
"license": "MIT", | ||
"authors": [ | ||
{ | ||
"name": "Youssef Ben Othman", | ||
"email": "[email protected]" | ||
} | ||
], | ||
"require": { | ||
"php": ">=5.5.0", | ||
"slim/slim": "^3.1", | ||
"slim/php-view": "^2.0", | ||
"monolog/monolog": "^1.17", | ||
"stripe/stripe-php": "^6.31" | ||
}, | ||
"autoload": { | ||
"psr-4": { | ||
"Store\\": "store/" | ||
} | ||
}, | ||
"config": { | ||
"process-timeout" : 0 | ||
}, | ||
"scripts": { | ||
"start": "php -S localhost:8888 index.php" | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
<?php | ||
use Slim\Http\Request; | ||
use Slim\Http\Response; | ||
use Stripe\Stripe; | ||
use Store\Inventory; | ||
use Store\Shipping; | ||
|
||
// Due to a bug in the PHP embedded server, URLs containing a dot don't work | ||
// This will fix the missing variable in that case | ||
if (PHP_SAPI == 'cli-server') { | ||
$_SERVER['SCRIPT_NAME'] = 'index.php'; | ||
} | ||
|
||
require __DIR__ . '/vendor/autoload.php'; | ||
|
||
// Instantiate the app | ||
$settings = require __DIR__ . '/settings.php'; | ||
$app = new \Slim\App($settings); | ||
|
||
// Instantiate the logger as a dependency | ||
$container = $app->getContainer(); | ||
$container['logger'] = function ($c) { | ||
$settings = $c->get('settings')['logger']; | ||
$logger = new Monolog\Logger($settings['name']); | ||
$logger->pushProcessor(new Monolog\Processor\UidProcessor()); | ||
$logger->pushHandler(new Monolog\Handler\StreamHandler($settings['path'], $settings['level'])); | ||
return $logger; | ||
}; | ||
|
||
// Middleware | ||
$app->add(function ($request, $response, $next) { | ||
Stripe::setApiKey($this->get('settings')['stripe']['secretKey']); | ||
$request = $request->withAttribute('staticDir', $this->get('settings')['stripe']['staticDir']); | ||
|
||
return $next($request, $response); | ||
}); | ||
|
||
// Serve the store | ||
$app->get('/', function (Request $request, Response $response, array $args) { | ||
return $response->write(file_get_contents($request->getAttribute('staticDir') . 'index.html')); | ||
}); | ||
|
||
// Serve static assets and images for index.html | ||
$paths = [ | ||
'javascripts' => 'text/javascript', 'stylesheets' => 'text/css', | ||
'images' => FILEINFO_MIME_TYPE, | ||
'images/products' => FILEINFO_MIME_TYPE, | ||
'images/screenshots' => FILEINFO_MIME_TYPE | ||
]; | ||
$app->get('/{path:' . implode('|', array_keys($paths)) . '}/{file:[^/]+}', | ||
function (Request $request, Response $response, array $args) use ($paths) { | ||
$resource = $request->getAttribute('staticDir') . $args['path'] . '/' . $args['file']; | ||
if (!is_file($resource)) { | ||
$notFoundHandler = $this->get('notFoundHandler'); | ||
return $notFoundHandler($request, $response); | ||
} | ||
|
||
return $response->write(file_get_contents($resource)) | ||
->withHeader('Content-Type', $paths[$args['path']]); | ||
} | ||
); | ||
|
||
// General config | ||
$app->get('/config', function (Request $request, Response $response, array $args) { | ||
$config = $this->get('settings')['stripe']; | ||
return $response->withJson([ | ||
'stripePublishableKey' => $config['publishableKey'], | ||
'stripeCountry' => $config['accountCountry'], | ||
'country' => $config['defaultCountry'], | ||
'currency' => $config['shopCurrency'], | ||
'paymentMethods' => implode($config['paymentMethods'], ', '), | ||
'shippingOptions' => Shipping::getShippingOptions() | ||
]); | ||
}); | ||
|
||
// List of fake products on our fake shop | ||
// Used to display the user's cart and calculate the total price | ||
$app->get('/products', function (Request $request, Response $response, array $args) { | ||
return $response->withJson(Inventory::listProducts()); | ||
}); | ||
|
||
// List of fake products on our fake shop | ||
// Used to display the user's cart and calculate the total price | ||
$app->get('/products/{id}', function (Request $request, Response $response, array $args) { | ||
return $response->withJson(Inventory::getProduct($args['id'])); | ||
}); | ||
|
||
// Create the payment intent | ||
// Used when the user starts the checkout flow | ||
$app->post('/payment_intents', function (Request $request, Response $response, array $args) { | ||
$data = $request->getParsedBody(); | ||
try { | ||
$paymentIntent = \Stripe\PaymentIntent::create([ | ||
'amount' => Inventory::calculatePaymentAmount($data['items']), | ||
'currency' => $data['currency'], | ||
'payment_method_types' => $this->get('settings')['stripe']['paymentMethods'] | ||
]); | ||
|
||
return $response->withJson([ 'paymentIntent' => $paymentIntent ]); | ||
} catch (\Exception $e) { | ||
return $response->withJson([ 'error' => $e->getMessage() ])->withStatus(403); | ||
} | ||
}); | ||
|
||
// Update the total when selected a different shipping option via the payment request API | ||
$app->post('/payment_intents/{id}/shipping_change', function (Request $request, Response $response, array $args) { | ||
$data = $request->getParsedBody(); | ||
$amount = Inventory::calculatePaymentAmount($data['items']); | ||
$amount += Shipping::getShippingCost($data['shippingOption']['id']); | ||
try { | ||
$paymentIntent = \Stripe\PaymentIntent::update($args['id'], [ 'amount' => $amount ]); | ||
return $response->withJson([ 'paymentIntent' => $paymentIntent ]); | ||
} catch (\Exception $e) { | ||
return $response->withJson([ 'error' => $e->getMessage() ])->withStatus(403); | ||
} | ||
}); | ||
|
||
// Fetch the payment intent status | ||
// Used for redirect sources when coming back to the return URL | ||
$app->get('/payment_intents/{id}/status', function (Request $request, Response $response, array $args) { | ||
$paymentIntent = \Stripe\PaymentIntent::retrieve($args['id']); | ||
return $response->withJson([ 'paymentIntent' => [ 'status' => $paymentIntent->status ] ]); | ||
}); | ||
|
||
// Events receiver for payment intents and sources | ||
$app->post('/webhook', function (Request $request, Response $response, array $args) { | ||
$logger = $this->get('logger'); | ||
|
||
$event = $request->getParsedBody(); | ||
|
||
// Parse the message body (and check the signature if possible) | ||
$webhookSecret = $this->get('settings')['stripe']['webhookSecret']; | ||
if ($webhookSecret) { | ||
try { | ||
$event = \Stripe\Webhook::constructEvent( | ||
$request->getBody(), | ||
$request->getHeaderLine('stripe-signature'), | ||
$webhookSecret | ||
); | ||
} catch (\Exception $e) { | ||
return $response->withJson([ 'error' => $e->getMessage() ])->withStatus(403); | ||
} | ||
} else { | ||
$event = $request->getParsedBody(); | ||
} | ||
|
||
$type = $event['type']; | ||
$object = $event['data']['object']; | ||
|
||
switch ($object['object']) { | ||
case 'payment_intent': | ||
$paymentIntent = $object; | ||
if ($type == 'payment_intent.succeeded') { | ||
// Payment intent successfully completed | ||
$logger->info('🔔 Webhook received! Payment for PaymentIntent ' . | ||
$paymentIntent['id'] . ' succeeded'); | ||
} elseif ($type == 'payment_intent.payment_failed') { | ||
// Payment intent completed with failure | ||
$logger->info('🔔 Webhook received! Payment on source ' . $paymentIntent['last_payment_error']['source']['id'] . | ||
' for PaymentIntent ' . $paymentIntent['id'] . ' failed'); | ||
} | ||
break; | ||
case 'source': | ||
$source = $object; | ||
if (!isset($source['metadata']['paymentIntent'])) { | ||
// Could be a source from another integration | ||
$logger->info('🔔 Webhook received! Source ' . $source['id'] . | ||
' did not contain any payment intent in its metadata, ignoring it...'); | ||
continue; | ||
} | ||
|
||
// Retrieve the payment intent this source was created for | ||
$paymentIntent = \Stripe\PaymentIntent::retrieve($source['metadata']['paymentIntent']); | ||
|
||
// Check the source status | ||
if ($source['status'] == 'chargeable') { | ||
// Source is chargeable, use it to confirm the payment intent if possible | ||
if (!in_array($paymentIntent->status, [ 'requires_source', 'requires_payment_method' ])) { | ||
$info = "PaymentIntent {$paymentIntent->id} already has a status of {$paymentIntent->status}"; | ||
$logger->info($info); | ||
return $response->withJson([ 'info' => $info ])->withStatus(200); | ||
} | ||
|
||
$paymentIntent->confirm([ 'source' => $source['id'] ]); | ||
} elseif (in_array($source['status'], [ 'failed', 'canceled' ])) { | ||
// Source failed or has been canceled, cancel the payment intent to let the polling know | ||
$logger->info('🔔 Webhook received! Source ' . $source['id'] . | ||
' failed or has been canceled, canceling PaymentIntent ' . $paymentIntent->id); | ||
$paymentIntent->cancel(); | ||
} | ||
break; | ||
} | ||
|
||
return $response->withJson([ 'status' => 'success' ])->withStatus(200); | ||
}); | ||
|
||
// Run app | ||
$app->run(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
|
||
; Find them at https://dashboard.stripe.com/account/apikeys | ||
publishableKey = | ||
secretKey = | ||
|
||
; Find it at https://dashboard.stripe.com/account/webhooks | ||
webhookSecret = | ||
|
||
; Your account country | ||
accountCountry = US | ||
|
||
; Your shop currency | ||
shopCurrency = usd | ||
|
||
; The default country selected for the billing address | ||
defaultCountry = US |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
<?php | ||
$iniFilename = __DIR__ . '/settings.ini'; | ||
if (!is_file($iniFilename)) { | ||
die('Missing settings.ini file.'); | ||
} | ||
|
||
$settings = parse_ini_file($iniFilename); | ||
if (!$settings) { | ||
die('Unable to read settings.ini file. Please check file format and read access.'); | ||
} | ||
|
||
return [ | ||
'settings' => [ | ||
'displayErrorDetails' => true, // set to false in production | ||
'addContentLengthHeader' => false, // Allow the web server to send the content-length header | ||
|
||
// Monolog settings | ||
'logger' => [ | ||
'name' => 'slim-app', | ||
'path' => isset($_ENV['docker']) ? 'php://stdout' : __DIR__ . '/logs/app.log', | ||
'level' => \Monolog\Logger::DEBUG, | ||
], | ||
|
||
'stripe' => [ | ||
// You shouldn't have to touch this | ||
'apiVersion' => '2019-03-14', | ||
|
||
// Update this path if you want to move your public folder | ||
'staticDir' => __DIR__ . '/../../public/', | ||
|
||
// Adapt these to match your account payments settings | ||
// https://dashboard.stripe.com/account/payments/settings | ||
'paymentMethods' => [ | ||
// 'ach_credit_transfer', // usd (ACH Credit Transfer payments must be in U.S. Dollars) | ||
'alipay', // aud, cad, eur, gbp, hkd, jpy, nzd, sgd, or usd. | ||
'bancontact', // eur (Bancontact must always use Euros) | ||
'card', // many (https://stripe.com/docs/currencies#presentment-currencies) | ||
'eps', // eur (EPS must always use Euros) | ||
'ideal', // eur (iDEAL must always use Euros) | ||
'giropay', // eur (Giropay must always use Euros) | ||
'multibanco', // eur (Multibanco must always use Euros) | ||
// 'sepa_debit', // Restricted. See docs for activation details: https://stripe.com/docs/sources/sepa-debit | ||
'sofort', // eur (SOFORT must always use Euros) | ||
'wechat' // aud, cad, eur, gbp, hkd, jpy, sgd, or usd. | ||
], | ||
|
||
// See settings.ini | ||
'publishableKey' => $settings['publishableKey'], | ||
'secretKey' => $settings['secretKey'], | ||
'webhookSecret' => $settings['webhookSecret'], | ||
'accountCountry' => $settings['accountCountry'], | ||
'shopCurrency' => $settings['shopCurrency'], | ||
'defaultCountry' => $settings['defaultCountry'] | ||
] | ||
], | ||
]; |
Oops, something went wrong.