Docker beginner gotchas

I started using Docker recently and there was one thing I didn’t understand – initially.

So, when running your Dockerfile if you don’t have a CMD command the container will continue to run. However, when you include CMD command the container will exit unless you run one of your programs in the foreground. So, you’ll have to do something like this:

CMD bash -c "apachectl -D FOREGROUND"

if you are running Apache, of course, that will be different if you are running different software.

Another thing is that the CMD is where you want to run you dependencies installation, like PHP’s composer intall and NOT in the RUN commands. Here is how my CMD looked like in the end:

CMD bash -c "composer install && chmod -R 777 /path/to/project/storage/ /path/to/project/bootstrap/ && php /path/to/project/artisan migrate --force && apachectl -D FOREGROUND"

Note that I am a complete noob when it comes to Docker so take this with a grain of salt. This is only my noob understanding.

Validating array input fields with Laravel

I was working on project which had to validate array input fields, which looked like so:

<input type="text" name="tags[0][id]" value="5" />
<input type="text" name="tags[1][id]" value="6" />

The normal way to validate this is by using a rule like this:

"tags.*.id" => ["required", "exists:tags:id"]

but in this case the required doesn’t work. So, you have to do this as well:

"tags" => ["required"]

I discovered this somewhere on the internet but I can’t remember where so credit goes to Unknown.

Changing Laravel’s default path for S3 uploads

Recently, I was working with Spatie‘s media library package which allows you to associate models with files and everything worked as in the docs but it was generating URLs for my media files of this sort:

https://bucket-name.s3.amazonaws.com/var/www/html/media/hash/file.png

and I wanted it to be like this:

https://bucket-name.s3.amazonaws.com/uploads/media/hash/file.png

so I first I figured there must be something wrong with the custom path generator I created but it turns out that Laravel controls that part of the URL through the disk options in “/config/filesystems.php”. So, I have a media disk there, that looks like this

'media' => [
  'driver' => 's3', 
  'root' => 'uploads/', // <- NOTICE THIS LINE it used to be public_path('media')
  'url' => env('AWS_S3_URL').'media', 
  'key' => env('AWS_ACCESS_KEY_ID'), 
  'secret' => env('AWS_SECRET_ACCESS_KEY'), 
  'region' => env('AWS_DEFAULT_REGION'), 
  'bucket' => env('AWS_BUCKET'), 
  'url' => env('AWS_URL'), 
  'endpoint' => env('AWS_ENDPOINT'), 
  'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false), 
],

notice the ‘root’ => ‘uploads/’ that part controls the first part of the S3 upload URL’s path.

Problems copying Laravel Sail project from fresh install into existing project

Today, I wanted to get a fresh Laravel 8 install on a project I started recently, basically restarting, as I needed the teams functionality of Jetstream and you can’t add the teams functionality into a project which already has Jetstream. So, I tried creating a fresh Laravel install and copying the files over to my project but for some reason the MySQL container didn’t get installed, which was confusing at first. Anyway, I ended up deleting the ‘vendor’ folder and installing everything a new with composer and then running:

php artisan sail:install

then I run

./vendor/bin/sail build --no-cache

and then just

./vendor/bin/sail up

this time MySQL worked and I could run my migrations.

I am sharing this in case someone else encounters a similar problem.

How to make repeatable custom fields in WordPress without paying for Advanced Custom Fields (ACF)

I was using the free version of Advanced Custom Fields (ACF) when I needed a way to make a gallery field where you select some images, order them and it displays as a gallery on a certain page and that’s impossible with the free version of ACF. I couldn’t pay for the Pro version of ACF either, so I looked for an alternative and I found Custom Field Suite. It has a good amount of the basic fields plus a repeatable/loop field which lets you repeat groups of the basic fields, incredibly handy!

Webpack causes: jQuery not defined

Recently, I started using Webpack and I was requiring jQuery using ‘require(“jquery”)’ and then I started getting errors in my app like so:

jQuery not defined or $ not defined

Then I found this piece here which explained that Webpack doesn’t set any global variables, so you have to set them yourself. So I did this:

window.jQuery = require('jquery');
window.$ = window.jQuery;

Happily this solved my issues.

Problems integrating PayPal

Recently, I’ve been trying to integrate PayPal subscriptions into a website of mine but I’ve hit a few problems which I’ve managed to resolve thanks to PayPal’s support. Hopefully this will help others. Here we go.

I followed this integration guide and I’ve managed to create a product and a plan and then subscription and then use the link from the subscription to redirect to PayPal. However, when I tried to pay for the subscription using my sandbox account I got this error:

We're sorry, we couldn't set up your subscription using the funding source you've selected. Please try another funding source.

I was baffled for a while but then I contacted PayPal’s support which told me that I don’t have balance in the same currency in which was the subscription (my case was euro) so I went on this page and I created a new sandbox account based in Germany which automatically filled the account with 5000 euro. However, when I tried to pay again for the subscription using the new account I hit another similar problem, here is the error:

Sorry, we couldn't set up your subscription using the payment method you selected. Please try another payment method.


So I contacted PayPal’s support again and after some back and forth they told me that the sandbox account wasn’t verified and that they’ve verified it and now it was working, which I tested immediately and yes it was working.

Problem is, why sandbox accounts with automatically generated email addresses, which look something like this: [email protected], have to be verified at all and how can you do that if you don’t own the email address? Baffling. Even worse, I have a sandbox account set up with one of my email addresses, which is not verified, and I tried to verify it but I never got a verification email!

So, generally, I am quite disappointed by PayPal even though I’ve managed to solved my problem. Lack of documentation is frustrating. I even found pieces of their documentation that simply don’t work.

Integrating PayPal subscriptions NOT the right way

Recently, I started trying to integrate PayPal subsriptions into a project of mine, however, it turns out I’ve started by using the old PayPal PHP SDK which is not the recommended way anymore. The recommended way is using the new Checkout PHP SDK, however that one doesn’t support subscriptions so the way to integrate PayPal subscriptions is by making direct https calls to their API. In this post I’ll show you how I started integrating the subscriptions using the old, deprecated SDK in case somebody needs it. Note that it’s not full integration (I’ve only gotten to a certain point) and it’s also Laravel specific.

<?php

namespace App\Logic\Paypal;

use Exception;
use App\Setting;
use PayPal\Api\Currency;
use PayPal\Api\MerchantPreferences;
use PayPal\Api\PaymentDefinition;
use PayPal\Api\Plan;
use PayPal\Api\Patch;
use PayPal\Api\PatchRequest;
use PayPal\Api\VerifyWebhookSignature;
use PayPal\Api\Webhook;
use PayPal\Auth\OAuthTokenCredential;
use PayPal\Common\PayPalModel;
use PayPal\Api\Agreement;
use PayPal\Api\Payer;
use PayPal\Rest\ApiContext;

class Paypal {

    public $apiContext;

    public function __construct()
    {
        $this->apiContext = new ApiContext(
            new OAuthTokenCredential(
                config('services.paypal.client_id'),     // ClientID
                config('services.paypal.secret')
            )
        );
    }

    public function createPlan()
    {
        $monthlyAmount = 10;
        $currency = 'EUR';

        // Create a new instance of Plan object
        $plan = new Plan();

        // # Basic Information
        // Fill up the basic information that is required for the plan
        $plan->setName('Epic Plan')
            ->setDescription('Unlimited form submissions. €10 paid monthly.')
            ->setType('infinite');

        // # Payment definitions for this billing plan.
        $paymentDefinition = new PaymentDefinition();

        $paymentDefinition->setName('Regular Payments')
            ->setType('REGULAR')
            ->setFrequency('Day')
            ->setFrequencyInterval("1")
            ->setCycles("0")
            ->setAmount(new Currency(array('value' => $monthlyAmount, 'currency' => $currency)));

        $merchantPreferences = new MerchantPreferences();
        $baseUrl = env('APP_URL');

        $merchantPreferences->setReturnUrl("$baseUrl/paypal/execute-agreement?success=true")
            ->setCancelUrl("$baseUrl/paypal/execute-agreement?success=false")
            ->setAutoBillAmount("yes")
            ->setInitialFailAmountAction("CONTINUE")
            ->setMaxFailAttempts("0")
            ->setSetupFee(new Currency(array('value' => $monthlyAmount, 'currency' => $currency)));

        $plan->setPaymentDefinitions(array($paymentDefinition));
        $plan->setMerchantPreferences($merchantPreferences);

        // ### Create Plan
        $output = $plan->create($this->apiContext);

        return $output;
    }

    public function activatePlan()
    {
        $createdPlan = $this->createPlan();

        $patch = new Patch();

        $value = new PayPalModel('{
           "state":"ACTIVE"
         }');

        $patch->setOp('replace')
            ->setPath('/')
            ->setValue($value);
        $patchRequest = new PatchRequest();
        $patchRequest->addPatch($patch);

        $createdPlan->update($patchRequest, $this->apiContext);

        $plan = Plan::get($createdPlan->getId(), $this->apiContext);

        // try to find previous setting with plan id
        $setting = Setting::where('name', '=', 'active_plan_id')->first();

        // save the plan id in the database
        if ($setting) {
            $setting->value = $plan->getId();
            $setting->save();
        } else {
            $newSetting = new Setting(
                [
                    'name' => 'active_plan_id',
                    'value' => $plan->getId(),
                ]
            );

            $newSetting->save();
        }

        return $plan;
    }

    public function createBillingAgreement()
    {
        $setting = Setting::where('name', '=', 'active_plan_id')->first();

        if (! $setting) {
            throw new Exception('Active plan id not found.');
        }

        $activePlanId = $setting->value;

        $agreement = new Agreement();

        $dateAgreementStarts = gmdate("Y-m-d\TH:i:s\Z", time() + 120);
        $agreement->setName('Base Agreement')
            ->setDescription('€10 a month in return for accessing our unlimited plan.')
            ->setStartDate($dateAgreementStarts);

        // Add Plan ID
        // Please note that the plan Id should be only set in this case.
        $plan = new Plan();
        $plan->setId($activePlanId);
        $agreement->setPlan($plan);

        // Add Payer
        $payer = new Payer();
        $payer->setPaymentMethod('paypal');
        $agreement->setPayer($payer);

        // ### Create Agreement
        // Please note that as the agreement has not yet activated, we wont be receiving the ID just yet.
        $agreement = $agreement->create($this->apiContext);

        // ### Get redirect url
        $approvalUrl = $agreement->getApprovalLink();

        return redirect($approvalUrl);
    }

    /**
     * Get list of all webhooks
     *
     * @return \PayPal\Api\WebhookList
     */
    public function listAllWebhooks()
    {
        $output = Webhook::getAllWithParams([], $this->apiContext);

        return $output;
    }

    /**
     * Delete all webhooks
     *
     * @return bool
     */
    public function deleteAllWebhooks()
    {
        $webhookList = $this->listAllWebhooks();

        foreach ($webhookList->getWebhooks() as $webhook) {
            $webhook->delete($this->apiContext);
        }

        return true;
    }

    /**
     * Register primary webhooks
     *
     * @return Webhook|string
     */
    public function registerPrimaryWebhooks()
    {
        $setting = Setting::where('name', '=', 'primary_webhooks_registered')->first();

        if ($setting) {
            return 'primary webhooks already registered';
        }

        $baseUrl = env('APP_URL');
        $webhook = new Webhook();
        $webhook->setUrl("$baseUrl/paypal/webhooks/primary");

        // # Event Types
        // Event types correspond to what kind of notifications you want to receive on the given URL.
        $webhookEventTypes = array();
        $webhookEventTypes[] = new \PayPal\Api\WebhookEventType(
            '{
                "name":"PAYMENT.AUTHORIZATION.CREATED"
            }'
        );
        $webhookEventTypes[] = new \PayPal\Api\WebhookEventType(
            '{
                "name":"PAYMENT.AUTHORIZATION.VOIDED"
            }'
        );
        $webhook->setEventTypes($webhookEventTypes);

        // ### Create Webhook
        $output = $webhook->create($this->apiContext);

        $newSetting = new Setting(
            [
                'name' => 'primary_webhooks_registered',
                'value' => $output->getId(),
            ]
        );
        $newSetting->save();

        return $output;
    }

    public function validatePrimaryWebhooks()
    {
        $setting = Setting::where('name', '=', 'primary_webhooks_registered')->first();

        if (! $setting) {
            throw new Exception('Primary webhooks not registered.');
        }

        $webhookId = $setting->value;

        $requestBody = file_get_contents('php://input');
        $headers = getallheaders();

        $headers = array_change_key_case($headers, CASE_UPPER);

        $signatureVerification = new VerifyWebhookSignature();
        $signatureVerification->setAuthAlgo($headers['PAYPAL-AUTH-ALGO']);
        $signatureVerification->setTransmissionId($headers['PAYPAL-TRANSMISSION-ID']);
        $signatureVerification->setCertUrl($headers['PAYPAL-CERT-URL']);
        $signatureVerification->setWebhookId($webhookId);
        $signatureVerification->setTransmissionSig($headers['PAYPAL-TRANSMISSION-SIG']);
        $signatureVerification->setTransmissionTime($headers['PAYPAL-TRANSMISSION-TIME']);

        $signatureVerification->setRequestBody($requestBody);

        $output = $signatureVerification->post($this->apiContext);

        $status = $output->getVerificationStatus();

        (new Setting(
            [
                'name' => 'webhook ' . date('Y-m-d H:i:s'),
                'value' => $status . $requestBody,
            ]
        ))->save();

        return response()->json([
            'status' => 'OK'
        ]);
    }
}

Here is the code. I’ll explain the four main methods in the class. First, ‘activatePlan’ – this will create the billing plan and activate it. Second, ‘registerPrimaryWebhooks’ – which will register some webhooks with PayPal. Third, ‘validatePrimaryWebhooks’ – which will validate/handle the primary webhooks, when one is received. And last, ‘createBillingAgreement’ – which will create a billing agreement and return a redirect to the PayPal’s approval link for that agreement.

Here is also my ‘PaypalController’ which handles the requests and also has some PayPal logic to execute the billing agreement once the user is redirected to the success url after approval.

<?php

namespace App\Http\Controllers;

use App\Setting;
use App\Logic\Paypal\Paypal;
use Illuminate\Http\Request;
use PayPal\Api\Agreement;

class PaypalController extends Controller
{
    public function createAndActivatePlan()
    {
        $setting = Setting::where('name', '=', 'active_plan_id')--->first();

        if ($setting) {
            return 'plan already activated';
        }

        $paypal = new Paypal();
        $plan = $paypal->activatePlan();

        if ($plan) {
            return 'success';
        } else {
            return 'failure';
        }
    }

    public function redirectToPaypal()
    {
        $paypal = new Paypal();

        return $paypal->createBillingAgreement();
    }

    public function executeAgreement()
    {
        // ## Approval Status
        // Determine if the user accepted or denied the request
        if (isset($_GET['success']) && $_GET['success'] == 'true') {
            $paypal = new Paypal();
            $token = $_GET['token'];
            $agreement = new Agreement();

            // ## Execute Agreement
            // Execute the agreement by passing in the token
            $agreement->execute($token, $paypal->apiContext);

            // ## Get Agreement
            // Make a get call to retrieve the executed agreement details
            $agreement = Agreement::get($agreement->getId(), $paypal->apiContext);

            dd($agreement);
        }
    }

    public function registerPrimaryWebhooks()
    {
        $paypal = new Paypal();

        return $paypal->registerPrimaryWebhooks();
    }

    public function handlePrimaryWebhooks()
    {
        $paypal = new Paypal();

        return $paypal->validatePrimaryWebhooks();
    }
}

Here is also my PayPal routes in case you need it

<?php

// PayPal
Route::get('/paypal/create-and-activate-plan', '[email protected]');
Route::get('/paypal/register-primary-webhooks', '[email protected]');
Route::get('/paypal/redirect-to-paypal', '[email protected]');
Route::get('/paypal/execute-agreement', '[email protected]');
Route::post('/paypal/webhooks/primary', '[email protected]');