DC Blog Blog RSS Feed https://dcblog.dev Write an SEO-friendly title: A quick start guide

Infographic displayed on a tablet in a business

As a website owner or content creator, you know how important it is to write great titles. Not only do titles need to be reader-friendly so that people will actually want to click on them, but they also need to be search engine friendly in order to ensure good click-through rates and rankings. So how do you strike the perfect balance? By following some best practices for writing SEO-friendly titles, of course! Keep reading to learn more.

Including Focus Keywords

One of the most important things you can do when writing an SEO-friendly title is to include focus keywords. Focus keywords are the words or phrases that best describe your page's content and are most likely to be used by people when searching for something related to your page. However, it's important to use focus keywords sparingly—too many keywords will not only turn off readers but also trigger search engine penalties for keyword stuffing.

Considering Length

In addition to including focus keywords, another important factor to consider when writing SEO-friendly titles is length. We're not talking about character count here—although that's important, too—but rather the number of words in your title. A good rule of thumb is to keep your titles between 50 and 60 characters so that they're easy to read and don't get cut off in search engine results pages. Of course, there will be exceptions to this rule, but as a general guideline, it's a good one to follow.

Using Emotional Hooks

Titles are designed to grab readers' attention and entice them to click through to your article or website. One way you can achieve this is by using emotional hooks in your titles. Emotional hooks are words or phrases that evoke an emotional response in readers and make them want to learn more about what you have to say. For example, if you're writing about ways to save money on groceries, a title like "50 Ways You're Wasting Money at the Supermarket" is likely to get more clicks than a boring, generic title like "How You Can Save Money on Groceries." 

Capitalizing Properly

Another important element of writing an SEO-friendly title is ensuring that you're using proper capitalization. There are a few different schools of thought on this subject, but as a general rule of thumb, you should use sentence case (aka lowercase with initial caps) for most titles.

However, there are exceptions—such as when using all caps or small caps—so be sure to familiarize yourself with the different types of capitalization before settling on a style for your own titles. 

Creating Title Tags That Match Your Brand Voice

A title tag is the HTML element that tells search engines what your page is about. Your title tag should match your brand voice so that readers recognize your content when they see it in a search engine result page. For example, if you have a blog titled "The Chilling World of Horror Movies," then your title tag should reflect that chilling tone. 

Structuring your blog post and title

Every blogger knows that a well-structured post is key to keeping readers engaged. But what makes a good structure? And how can you make sure your post is easy to follow?

In this guide, we'll show you how to structure a blog post for maximum impact. Plus, we'll give you some tips on creating a catchy headline. Let's get started!

infographics on structuring a blog post

Conclusion 

By following these best practices for writing SEO-friendly titles, you can help ensure that your pages get seen by both readers and search engines alike. So don't underestimate the power of an effective title—craft yours carefully and watch your traffic increase!

]]>
Sun, 20 Nov 2022 05:31:00 GMT https://dcblog.dev/write-an-seo-friendly-title-a-quick-start-guide https://dcblog.dev/write-an-seo-friendly-title-a-quick-start-guide
Use PHP to generate table of contents from heading tags

I wanted to create a table of contents from the post's heading tags automatically without having to change the headings like adding an id or anchor.

I started to look at existing solutions and came across this custom function on Stackoverflow by sjaak-wish

function generateIndex($html) {
    preg_match_all('/<h([1-6])*[^>]*>(.*?)<\/h[1-6]>/',$html,$matches);

    $index = "<ul>";
    $prev = 2;

    foreach ($matches[0] as $i => $match){

        $curr = $matches[1][$i];
        $text = strip_tags($matches[2][$i]);
        $slug = strtolower(str_replace("--","-",preg_replace('/[^\da-z]/i', '-', $text)));
        $anchor = '<a name="'.$slug.'">'.$text.'</a>';
        $html = str_replace($text,$anchor,$html);

        $prev <= $curr ?: $index .= str_repeat('</ul>',($prev - $curr));
        $prev >= $curr ?: $index .= "<ul>";

        $index .= '<li><a href="#'.$slug.'">'.$text.'</a></li>';

        $prev = $curr;
    }

    $index .= "</ul>";

    return ["html" => $html, "index" => $index];
}

This will generate a list of links based on the headings.

To use the function call it and pass in your content, then specify the array key to use. html for the body and index for the table of contents.

Table of contents:

toc($post->content)['index']

Content:

toc($post->content)['html']

 

]]>
Tue, 15 Nov 2022 11:43:00 GMT https://dcblog.dev/use-php-to-generate-table-of-contents-from-heading-tags https://dcblog.dev/use-php-to-generate-table-of-contents-from-heading-tags
Running Docker on M1 Mac - docker: compose is not a docker

When upgrading from an Intel mac to an Apple Silicone I noticed docker fails to run. I'm using Laravel Sail when Sail is installed or when a sail up command is attempted I get an error: 

docker: 'compose' is not a docker command

The reason for this is a docker-compose is now a plugin. Docker needs docker-compose to be installed. Use brew to install docker-compose

brew install docker-compose

Once installed sail will function normally without any extra setup.

]]>
Sun, 13 Nov 2022 21:34:00 GMT https://dcblog.dev/running-docker-on-m1-mac-docker-compose-is-not-a-docker https://dcblog.dev/running-docker-on-m1-mac-docker-compose-is-not-a-docker
Using Laravel Sail alongside PhpStorm

I'm trying out Laravel Sail for my local development, this post serves as documenting the process and getting PhpStorm to play nice with Sail.

Sail is a tool for using docker without needing to use docker commands directly, it builds from an image that installs the latest versions of PHP, Nginx and MySQL. 

watch a video version;

Sail is a Laravel package, which means you can use a fresh version of Laravel or install it in existing projects. 

You will need docker to be installed, if you don't have docker you can download it at https://www.docker.com/

Note you still need PHP installed so you can run composer to install sail on existing projects.

Install Sail

Download Sail package

Use —dev to install into dev dependencies. 

composer require laravel/sail --dev

Install Sail

php artisan sail:install

Select MySQL from the list of options

Start sail, the first time you run this it will take a while whilst all the files are downloaded, future calls are much quicker.

./vendor/bin/sail up

You may have to chase the DB_HOST in .env to mysql which is the host name created by sail.

Setup Testing

Change phpunit.xml to use a testing database, sail created the database. This means every time a test runs a database named testing will be used.

<server name=“DB_DATABASE” value=“testing”/> 

MySQL Access

In order to connect to the database from outside the container the -d command may be needed mean to run detached

sail up -d 

Then connect using:

host: 127.0.0.1
username: sail
password: password

 

Configuring PHPStorm to use docker

Configure Test Runner

Open settings select PHP -> Test Frameworks

Click the plus and select the remote connection

Next select the sail container

Select docker

Then select the sail image

This will tell storm to use docker for CLI actions.

You may find that storm cannot run tests using its test runner, so let's configure it now.

Edit your test configuration 

For the default interpreter select docker followed by the image.

You may need to configure docker to use the image name

 

Enter the network name.

Find the connection name in a terminal run

docker network ls

This sits the container names the name you’re looking for is the name of the project followed by like dcblog_sail

]]>
Sat, 12 Nov 2022 01:29:00 GMT https://dcblog.dev/using-laravel-sail-alongside-phpstorm https://dcblog.dev/using-laravel-sail-alongside-phpstorm
Handle Stripe checkout webhooks

Continuing on from my last post Sell products with Stripe let's first setup a webhook on stripe by going to Developers -> Webhooks https://dashboard.stripe.com/webhooks 

Add a new webhook, provide a URL for the webhook to go to such as https://domain.com/webhooks/stripe

select the events to listen to since I'm dealing with the hosted checkout for one-off products I want the checkout session.checkout.completed event.

Once created click into the webhook and press reveal under signing secret to reveal the webhook API key. Add this key to your .env file 

STRIPE_WEBHOOK_SECRET=

Next open App/Http/Middleware/VerifyCsrfToken.php to whitelist an endpoint to allow Stripe to send POST requests in.

class VerifyCsrfToken extends Middleware
{
    /**
     * The URIs that should be excluded from CSRF verification.
     *
     * @var array
     */
    protected $except = [
        'webhooks/stripe',
    ];
}

Create a route 

Route::post('webhooks/stripe', [WebhooksController::class, 'collect']);

In the controller create a method, set the stripe API key, secret and collect POST data using php//input

Then in a try-catch verify the webhook API key with Stripe.

public function collect()
{
    Stripe::setApiKey(config('services.stripe.secret'));
    $secret     = config('services.stripe.webhook');
    $payload    = file_get_contents("php://input");
    $sig_header = $_SERVER["HTTP_STRIPE_SIGNATURE"];
    $event      = null;

    try {
        $event = Webhook::constructEvent($payload, $sig_header, $secret);
    } catch (\UnexpectedValueException $e) {
        // Invalid payload
        http_response_code(400);
        return true;
    } catch (SignatureVerification $e) {
        // Invalid signature
        http_response_code(400); // PHP 5.4 or greater
        return true;
    }

    // Handle the checkout.session.completed event
    if ($event->type === 'checkout.session.completed') {
        $this->handle_checkout_session($event);
    }

    http_response_code(200);
}

Finally checking the $event->type matches the event checkout.session.completed all another method and pass in the event.

Inside the event drill down to the metadata which will contain any data sent to stripe, open you will put a user id and product in so you can process orders.

public function handle_checkout_session($eventData)
{
    $meta = $eventData->data->object->metadata;

    if (isset($meta->user_id)) {
        $purchase = Purchase::create([
            'user_id'    => $meta->user_id,
            'product_id' => $meta->product_id,
            'data'       => json_encode($eventData),
        ]);

        Mail::
            to($purchase->user->email)
            ->send(new PurchasedProduct($purchase));
    }
}

In this case, I create a transaction log and send an email to the customer.

The important thing is in the collect method to respond to Stripe as quick as possible by sending an HTTP status code and after sending a response process the data.

 

 

 

]]>
Sat, 12 Nov 2022 00:35:00 GMT https://dcblog.dev/handle-stripe-checkout-webhooks https://dcblog.dev/handle-stripe-checkout-webhooks
The New Reality of AI-Generated Content Creation

We're on the cusp of a new era in content creation, and it's all thanks to large language and image AI models. These so-called "generative AI" or "foundation models" are capable of producing text and images that can be used for everything from blog posts and social media posts to web copy, sales emails, and ads. In other words, these models have the potential to completely revolutionize the way businesses operate.

Codex, Chatbots, and Marketing

On the surface, it sounds like a dream come true for businesses and professionals who rely on content creation to drive their success. After all, who wouldn't want a machine that can crank out high-quality content at lightning-fast speeds? But before we get too ahead of ourselves, it's important to understand the reality of these AI-generated content models.

The Pros and Cons of AI-Generated Content

There's no denying that AI-generated content has the potential to be a game-changer in a number of industries. However, it's important to remember that these models are not perfect substitutes for human involvement in the creative process. In fact, there are both pros and cons to using AI-generated content. Let's take a closer look at some of each: 

Pros: 

• Increased Productivity: One of the biggest advantages of using AI-generated content is the increased productivity it can provide. With a machine doing the majority of the work, businesses will be able to churn out content at a much faster rate than ever before. 
• Improved Quality/Variety/Personalization of Content: Another benefit of using generative AI is the improved quality, variety, and personalization of content that it can provide. Thanks to the vast amount of data that these models have access to, they're able to produce content that is far superior to what humans could create on their own. 
• Cost savings: When you factor in the increased productivity and improved quality of content generated by these models, it's easy to see how businesses can save a significant amount of money by using them. 

Cons: 

• Limited creativity: One downside to using AI-generated content is the lack of creativity that these models are often unable to recreate. While they may be able to produce well-written or accurate pieces, they often lack the warmth or personality that humans can infuse into their work. 
• Lack of human connection: Another drawback of AI-generated content is the lack of human connection that comes with it. Because these pieces are often void of any real emotion or personality, they can fail to connect with readers on a deeper level. 
• Potential job loss: Perhaps one of the most concerning negatives associated with AI-generated content is the potential loss of jobs that could result from its use. As more businesses rely on these machines to produce their content, human jobs will undoubtedly be lost in the process. 

Wrapping up

While there is no doubt that large language and image AI models have created new opportunities for businesses, it's important to remember that they are not perfect substitutes for human involvement in the creative process. There are both pros and cons associated with using these models for content creation, so creators must weigh those factors carefully before making a decision."

]]>
Wed, 09 Nov 2022 17:53:00 GMT https://dcblog.dev/the-new-reality-of-ai-generated-content-creation https://dcblog.dev/the-new-reality-of-ai-generated-content-creation
Sell products with Stripe

In this tutorial, I will cover how to use Stripe to take payment for products using their hosted checkout.

The first step is to create a stripe account at https://dashboard.stripe.com/register

Once registered go to the developer's link and take note of the API keys.

Stripe has 2 different types of keys test keys and live keys. When in test mode the keys are prefixed with pk_test and sk_test. Use the test keys when testing your integration.

Stripe API Keys

Next, open a Laravel application.

Install Stripe SDK using composer

composer require stripe/stripe-php

Add your API keys to .env

#test keys
STRIPE_KEY=pk_test_...
STRIPE_SECRET=sk_test_...
STRIPE_WEBHOOK_SECRET=

#live keys
#STRIPE_KEY=
#STRIPE_SECRET=
#STRIPE_WEBHOOK_SECRET=

Open config/services.php add the following stripe array. This allows your code to refer to these keys using the format of config('services.stripe.key')

'stripe' => [
    'key'     => env('STRIPE_KEY'),
    'secret'  => env('STRIPE_SECRET'),
    'webhook' => env('STRIPE_WEBHOOK_SECRET'),
],

When you want to take a payment for a single product create a controller method to load a product based on its slug / id 

Select the product from the database, set the Stripe API key and pass in the secret.

Create a Session object using Session:create() 

Inside this object specify the line items, in this example I'm using a product defined in Stripe, refer to the product ID as the price in line_items.

Use metadata to pass any custom data that will be passed back in a webhook, this is a perfect place for a product / price or any other references you may want to use when fulfilling the order.

To add a product to stripe go to https://dashboard.stripe.com/test/products/create

<?php

namespace Modules\Products\Http\Controllers;

use Illuminate\Routing\Controller;
use Stripe\Checkout\Session;
use Stripe\Stripe;

class PayController extends Controller
{
    public function buy($slug)
    {
        $product = Product::where('slug', $slug)->firstOrFail();

        Stripe::setApiKey(config('services.stripe.secret'));

        $domain = config('app.url');

        $user = auth()->user();

        $checkout_session = Session::create([
            'line_items'            => [
                [
                    'price'    => $product->price_id,
                    'quantity' => 1,
                ]
            ],
            'mode'                  => 'payment',
            'allow_promotion_codes' => true,
            'metadata'              => [
                'product_id' => $product->id
            ],
            'customer_email'        => $user->email,
            'success_url'           => $domain.'/success',
            'cancel_url'            => $domain.'/cancel',
            'automatic_tax'         => [
                'enabled' => true,
            ],
        ]);

        return redirect()->away($checkoutSession->url);
    }

}

In order to go to the checkout redirect to $checkout_session->url this will take you to the hosted checkout.

If you want to handle multiple products that are not setup on Stripe, create a dynamic array of product data, in this example I need the product name, currency, value and quantity.

public function pay($products)
{
    Stripe::setApiKey(config('services.stripe.secret'));
    $user         = auth()->user();
    $productItems = [];
    $total = 0;
    $domain = config('app.url');

    foreach ($products as $item) {  
        $item->cash_price = $item->cash_price + $discount;
        
        $productItems[] = [
            'price_data' => [
                'product_data' => [
                    'name' => $item->name,
                ],
                'currency'     => 'gbp',
                'unit_amount'  => $item->cash_price * 100,
            ],
            'quantity' => $item->quantity
        ];  
    }

    $checkoutSession = Session::create([
        'line_items'            => [$productItems],
        'mode'                  => 'payment',
        'allow_promotion_codes' => true,
        'metadata'              => [
            'user_id' => $user->id
        ],
        'customer_email'        => $user->email,
        'success_url'           => $domain.'/success',
        'cancel_url'            => $domain.'/cancel',
    ]);

    return redirect()->away($checkoutSession->url);
}

 

]]>
Tue, 08 Nov 2022 07:23:00 GMT https://dcblog.dev/sell-products-with-stripe https://dcblog.dev/sell-products-with-stripe
Laravel update factory after creation

Using a Laravel factory to create a user and then update a relationship directly is possible using factory callbacks. Using a configure method you can call afterCreating and afterMaking closures:

<?php

namespace Database\Factories;

use App\Models\Tenant;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;

class UserFactory extends Factory
{
    protected $model = User::class;

    public function definition(): array
    {
        return [
            'name'           => $this->faker->name(),
            'slug'           => Str::slug($this->faker->name()),
            'email'          => $this->faker->email(),
            'password'       => Hash::make('password'),
            'is_active'      => 1,
            'remember_token' => Str::random(10),
        ];
    }

    public function configure()
    {
        return $this->afterCreating(function (User $user) {
            $user->tenant_id = Tenant::factory()->create([
                'owner_id' => $user->id
            ]);
            $user->save();
        });
    }
}

Read more about Factory Callbacks

]]>
Sat, 05 Nov 2022 08:02:00 GMT https://dcblog.dev/laravel-update-factory-after-creation https://dcblog.dev/laravel-update-factory-after-creation
Laravel boot multiple traits

Lets say you want to use multiple traits in your models to reuse common code such as applying UUIDs and settings global query scopes like this:

use HasUuid;
use HasTenant;

Each of these traits has a boot method:

HasUuid:

trait HasUuid
{
    public function getIncrementing(): bool
    {
        return false;
    }

    public function getKeyType(): string
    {
        return 'string';
    }

    public static function boot()
    {
        static::creating(function (Model $model) {
            // Set attribute for new model's primary key (ID) to an uuid.
            $model->setAttribute($model->getKeyName(), Str::uuid()->toString());
        });
    }
}

HasTenant:

trait HasTenant
{
    public static function boot()
    {
        if (auth()->check()) {
            $tenantId = auth()->user()->tenant_id;

            //assign when creating a record
            static::creating(function ($query) use ($tenantId) {
                $query->tenant_id = $tenantId;
            });

            //apply condition to all queries
            static::addGlobalScope('tenant', function (Builder $builder) use ($tenantId) {
                $builder->where('tenant_id', $tenantId);
            });
        }
    }
}

trying to run this will trigger an error: 

Trait method App\Models\Traits\HasTenant::boot has not been applied as App\Models\User::boot, because of collision with App\Models\Traits\HasUuid::boot

This is because both traits are using a boot method, to solve this Laravel supports a bootTraitName for example HasTenant becomes bootHasTenant

Change both traits to use the bootTraitName like this:

trait HasUuid
{
    public static function bootHasUuid()
    {

and

trait HasTenant
{
    public static function bootHasTenant()
    {

Now they can be used together in a single model without causing any errors.

Laravel also supports initializeTraitName which can look like:

protected function initializeHasToken()
{
    $this->token = Str::random(100);
}

 

]]>
Sat, 05 Nov 2022 05:54:00 GMT https://dcblog.dev/laravel-boot-multiple-traits https://dcblog.dev/laravel-boot-multiple-traits
Laravel how to test CSV download

When you have a CSV generated how do you test it runs. Take this extract:

public function export()
{
    $actions = $this->getData()->get();

    $columns = [
        'User',
        'Start Date',
        'End Date',
    ];

    $data = [];
    foreach ($actions as $action) {
        $data[] = [
            $action->user->name ?? '',
            $action->due_at,
            $action->completed_at,
        ];
    }

    $now      = Carbon::now()->format('d-m-Y');
    $filename = "actions-{$now}";

    return csv_file($columns, $data, $filename);
}

The above collects data and sends it to a helper function called csv_file which in turn will export a CSV file.

For completeness it contains:

if (! function_exists('csv_file')) {
    function csv_file($columns, $data, string $filename = 'export'): BinaryFileResponse
    {
        $file      = fopen('php://memory', 'wb');
        $csvHeader = [...$columns];

        fputcsv($file, $csvHeader);

        foreach ($data as $line) {
            fputcsv($file, $line);
        }

        fseek($file, 0);

        $uid = unique_id();

        Storage::disk('local')->put("public/$uid", $file);

        return response()->download(storage_path('app/public/'.$uid), "$filename.csv")->deleteFileAfterSend(true);

    }
}

Now there are 2 tests I want to confirm first, I get 200 status response to ensure the endpoint does not throw an error and secondly, the response contains the generated file name for the CSV.

To cater for the 200 status code run the endpoint and assertOk() which is a shortcut for a 200 instead of assertStatus(200)

Next check the header of the response and read the content-disposition header this will contain an attachment followed by the filename. Doing an assert true and comparing the header with the expected response;

test('can export actions', function () {
    $this->authenticate();

    $response = $this
        ->get('admin/reports/actions/csv')
        ->assertOk();

    $this->assertTrue($response->headers->get('content-disposition') == 'attachment; filename=actions-'.date('d-m-Y').'.csv');
});

Another way would be to use Pest's expect API:

$header = $response->headers->get('content-disposition');
$match = 'attachment; filename=actions-'.date('d-m-Y').'.csv';

expect($header)->toBe($match);

 

]]>
Mon, 11 Jul 2022 09:41:00 GMT https://dcblog.dev/laravel-how-to-test-csv-download https://dcblog.dev/laravel-how-to-test-csv-download
Laravel organise migrations into folders

When a project grows the migrations folder can contain a lot of migration, ever wanted to desperate them into folders? turns out it's easy to so. All you need to do is tell Laravel where to read the migrations from.

In your AppServiceProvider.php boot call, you can call $this->loadMigrationsFrom() and give it a path of all the folder locations:

$migrationsPath = database_path('migrations');
$directories    = glob($migrationsPath.'/*', GLOB_ONLYDIR);
$paths          = array_merge([$migrationsPath], $directories);

$this->loadMigrationsFrom($paths);

Now when you run

php artisan migrate

all folders will be scanned.

To migrate specific folders use --path for example for all migration in a folder called posts

php artisan migrate --path=/database/migrations/posts

or to make migration in a folder:

php artisan make:migration create_posts_table --path=/database/migrations/posts

 

]]>
Sun, 03 Jul 2022 12:23:00 GMT https://dcblog.dev/laravel-organise-migrations-into-folders https://dcblog.dev/laravel-organise-migrations-into-folders
Laravel how to set app environment during tests

If you need to set an environment to be a specific one such as staging you can override the environment by changing the config value app_env

config(['app.env' => 'staging']);

Then doing a dd on config(‘app.env’) will return that environment you’ve just set.

I haven’t found a way to set environments with app()->environment() so instead, I recommend using this in_array function and passing in the config(‘app.env’) and then specifically defining the environments. 

in_array(config('app.env'), ['local', 'staging'])

For example, say you have this in a command:

if (! in_array(config('app.env'), ['local', 'staging'])) {
    $this->error(‘Will only run on local and staging environments’);
    return true;
}

Test this runs when the environment is set to production

test(‘cannot run on production’, function () {

    config(['app.env' => 'production']);

    $this->artisan('db:production-sync')
        ->expectsOutput(‘DB sync will only run on local and staging environments’)
        ->assertExitCode(true);
});

 

]]>
Sun, 26 Jun 2022 10:59:00 GMT https://dcblog.dev/laravel-how-to-set-app-environment-during-tests https://dcblog.dev/laravel-how-to-set-app-environment-during-tests
Test Laravel Packages with PestPHP

PestPHP is a testing framework with a focus on simplicity, in this post I'll explain how to use PestPHP within a Laravel package to test its functionality.

Installation

In order to test a Laravel package, a package called testbench is required. Testbench ready more about testbench at https://packages.tools/testbench/getting-started/introduction.html#installation

Install testbench and PestPHP package

composer require orchestra/testbench --dev
composer require pestphp/pest --dev
composer require pestphp/pest-plugin-laravel --dev

Setup test environment

Inside composer.json autoload the tests directory I'm using Dcblogdev\\PackageName to represent the vendor username and repo name.

"autoload": {
    "psr-4": {
        "Dcblogdev\\PackageName\\": "src/",
        "Dcblogdev\\PackageName\\Tests\\": "tests"
    }
},

This will auto-load the tests directory at the root of the package.

Next, create a file called phpunit.xml. 

<?xml version="1.0" encoding="UTF-8"?>
<phpunit
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  bootstrap="vendor/autoload.php"
  backupGlobals="false"
  backupStaticAttributes="false"
  colors="true"
  verbose="true"
  convertErrorsToExceptions="true"
  convertNoticesToExceptions="true"
  convertWarningsToExceptions="true"
  processIsolation="false"
  stopOnFailure="false"
  xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
>
  <coverage>
    <include>
      <directory suffix=".php">src/</directory>
    </include>
  </coverage>
  <testsuites>
    <testsuite name="Unit">
      <directory suffix="Test.php">./tests/Unit</directory>
    </testsuite>
  </testsuites>
  <php>
    <env name="DB_CONNECTION" value="testing"/>
    <env name="APP_KEY" value="base64:2fl+Ktvkfl+Fuz4Qp/A75G2RTiWVA/ZoKZvp6fiiM10="/>
  </php>
</phpunit>

Here an APP_KEY is set, feel free to change this. Also, the Database connection is set to testing. This also sets the tests inside tests/Unit to be loaded. Add tests/Feature as needed.

Testing

Create a tests folder inside it create 2 files Pestp.php and TestCase.php

TestCase.php

<?php

namespace Dcblogdev\PackageName\Tests;

use Orchestra\Testbench\TestCase as Orchestra;
use Dcblogdev\PackageName\PackageNameServiceProvider;

class TestCase extends Orchestra
{
    protected function getPackageProviders($app)
    {
        return [
            PackageNameServiceProvider::class,
        ];
    }
}

 This loads the package service provider.

Pest.php

<?php

use Dcblogdev\PackageName\Tests\TestCase;

uses(TestCase::class)->in(__DIR__);

In Pest.php set any custom test helpers. This would load the TestCase and test in in the current tests directory using __DIR__ this allows pest to run in sub folders of tests ie Unit and Feature are the most common folders.

Next, create a folder called Unit inside tests and create a test file I'll use DemoTest.php

tests/Unit/DemoTest.php

test('confirm environment is set to testing', function () {
    expect(config('app.env'))->toBe('testing');
});

No imports or classes are required. That's what's great about Pest you can concentrate on writing the test with minimal setup required.

From here you can write your tests using Pest in the same manner as you would writing PestPHP tests in a Laravel application.

]]>
Sun, 26 Jun 2022 10:42:00 GMT https://dcblog.dev/test-laravel-packages-with-pestphp https://dcblog.dev/test-laravel-packages-with-pestphp
Laravel Sync Remote Database Package

I often want to sync a production database into a local database and manually exporting and importing, so I wrote a package to automate it!

A word of warning you should only sync a remote database into a local database if you have permission to do so within your organisation's policies. I'm syncing during early phases of development where the data is largely test data and not actual customer data.

I wrote a package called Laravel DB Sync

Install the package:

composer require dcblogdev/laravel-db-sync

Publish the config file

php artisan vendor:publish --provider="Dcblogdev\DbSync\DbSyncServiceProvider" --tag="config"

Set the remote database credentials in your .env file

When using SSH Add:

REMOTE_USE_SSH=true
REMOTE_SSH_PORT=22
REMOTE_SSH_USERNAME=
REMOTE_DATABASE_HOST=

REMOTE_DATABASE_USERNAME=
REMOTE_DATABASE_NAME=
REMOTE_DATABASE_PASSWORD=
REMOTE_DATABASE_IGNORE_TABLES=''

REMOTE_REMOVE_FILE_AFTER_IMPORT=true
REMOTE_IMPORT_FILE=true

For only MySQL remote connections:

REMOTE_DATABASE_HOST=
REMOTE_DATABASE_USERNAME=
REMOTE_DATABASE_NAME=
REMOTE_DATABASE_PASSWORD=
REMOTE_DATABASE_IGNORE_TABLES=''

REMOTE_REMOVE_FILE_AFTER_IMPORT=true
REMOTE_IMPORT_FILE=true

if you want to exclude certain tables you can add them to REMOTE_DATABASE_IGNORE_TABLES for example to ignore users and jobs being exported

REMOTE_DATABASE_IGNORE_TABLES='users,jobs'

now when you want to export the remote database into the local database run:

php artisan db:production-sync

https://github.com/dcblogdev/laravel-db-sync

]]>
Sun, 26 Jun 2022 05:50:00 GMT https://dcblog.dev/laravel-sync-remote-database-package https://dcblog.dev/laravel-sync-remote-database-package
How to Drive Traffic to Your Website in 2022: Tips for Web Developers

As any web developer knows, getting traffic to your site is essential. Without visitors, your site will never achieve its potential. One way to drive traffic to your site is through copywriting.

Everyone knows the importance of SEO which involves including the right keywords in titles, main body copy and alt tags - while avoiding keyword stuff.

A lesser-discussed strategy is copywriting, which is actually a very valuble tool in SEO.

Why is copywriting important and overcoming its challenges

Copywriting is the art of writing compelling and engaging content that encourages people to visit your site. By crafting compelling headlines and descriptions, you can entice people to click through to your site. Once they're on your site, it's important to keep them engaged with well-written and informative content.

As the internet continues to evolve, search engines are getting smarter and more sophisticated. They are now able to more accurately match people with the content they are looking for. This means that publishers need to create content that is not only keyword-rich but also engaging and interesting.

Gone are the days of simply stuffing a webpage with keywords in hopes of attracting traffic.

In recent years, one of the biggest changes has been the introduction of RankBrain. This artificial intelligence system analyses how people interact with search results, and uses this information to improve the ranking of pages.

One of the things that RankBrain looks at is how long people spend on a page. If people click on a result and then quickly return to the search results, it's a sign that they didn't find what they were looking for. This tells Google that the page isn't relevant or engaging, and so it will be ranked lower in future. 

Now, more than ever, it is important to create content that people will actually want to read.

This means including compelling images, interesting facts, and helpful information.

The process of copywriting can be a challenge for those who are not used to writing large amounts of text. However, the copywriting industry is moving at a fast pace, and there are now tools available that website owners can use to express themselves clearly and concisely. These tools can help to transform the process of copywriting, making it easier and more efficient. By using these tools, website owners can save valuable time and energy, while still producing high-quality content that will appeal to their target audience.

Examples include:

Jasper AI

A writing assistant that takes your basic premise for content and transforms it into marketable web content suitable for blogs and social media.

Grammarly

Ensure your web content meets a good standard of English for grammar and punctuation.

Surfer SEO

Use AI to analyse trends in what people are searching for and find paths to ranking for highly competitive keywords.

Tactics that work in 2022

Here are some copywriting tactics that you can use to drive traffic to your site in 2022:

Use short, punchy descriptions

Make your content easy to read and digest. In copywriting, word choice is everything. Short, punchy descriptions are easy for readers to digest and understand, making them more likely to engage with the text. In contrast, long-winded descriptions can be off-putting and difficult to follow.

When writing copy, it is therefore important to strike the right balance between conciseness and clarity. Using active language and specific detail can help to make complex concepts more accessible, without compromising on precision.

Include images and videos

In an age where visual media is increasingly prevalent, it's important to know how to incorporate images and videos into your copywriting. When used effectively, visuals can help to break up text and add intrigue for the reader. In addition, carefully chosen visuals can also help to underscore key points and add impact to your writing. However, it's important to use visuals thoughtfully - too many images can be overwhelming, and poorly chosen visuals can actually detract from your message. 

Focus on quality over quantity

In an age of ever-decreasing attention spans, it is more important than ever to focus on quality over quantity in copywriting. Gone are the days when readers had the patience to wade through pages of text; today's reader wants information that is clear, concise, and to the point. A well-written piece of copy will deliver this information in an engaging and persuasive manner, without resorting to cheap tricks or filler content.

Infuse a human touch

In a world that is increasingly driven by technology, it is easy to forget the importance of human connection. However, recent studies have shown that businesses that infuse a human touch into their communications are more likely to succeed.

As we move into 2022, this trend is only likely to continue. Consumers are becoming more and more aware of the importance of supporting businesses that treat them like people, not just faceless customers. In order to stay ahead of the curve, businesses need to start rethinking their approach to communication.

Instead of relying on technology to do the work for them, they need to focus on creating messages that resonate with their audience on a personal level. Only then will they be able to build the strong and lasting relationships that are essential for success in today's competitive marketplace.

Create thoughtful content

Today's consumers are more thoughtful in their decision-making process. They care about finding the right product or service to meet their needs, rather than making a quick decision.

As a result, copywriting needs to reflect the benefits of your service in a way that shows people why they should choose you, without putting added pressure into the decision-making process. By doing so, you will be more likely to convert prospects into customers.

Produce Informative copy

Copywriting should share knowledge freely, without using excessive gateways. People want to associate with businesses that properly understand them, so businesses need to become concise about who they can help, how they will help, and why it matters to the people they want to serve.

The goal is not to "trick" someone into buying something they don't need, but rather to educate them about a solution that can make their life better. Good copywriting will be clear, interesting, and easy to digest. It should answer the reader's questions and leave them feeling informed and confident about the product or service being offered. When done well, copywriting can be an invaluable tool for businesses of all sizes.

]]>
Sun, 26 Jun 2022 05:36:00 GMT https://dcblog.dev/how-to-drive-traffic-to-your-website-in-2022-tips-for-web-developers https://dcblog.dev/how-to-drive-traffic-to-your-website-in-2022-tips-for-web-developers
Generate PDF and Epub files using Pandoc

I write my books using Markdown. Using a tool called Pandoc you can convert Markdown files into PDFs and Epub files. Let's take a look at the commands.

Install Pandoc with Homebrew

brew install pandoc

Converte a .md file to .pdf aka generate a PDF

pandoc demo.md --pdf-engine=xelatex -o demo.pdf

Note when generating a PDF the option --pdf-engine is required.

The syntax is pandoc followed by the source file add any options with the flag. Set the output location and filename with the -o flag.

if you encounter this error:

pandoc: /Library/TeX/texbin/pdflatex: createProcess: posix_spawnp: illegal operation (Inappropriate ioctl for device)

It means xelatex is not installed on the machine.

To install xelatex:

brew tap homebrew/cask
brew install basictex
eval "$(/usr/libexec/path_helper)"
sudo tlmgr update --self
sudo tlmgr install texliveonfly
sudo tlmgr install xelatex
sudo tlmgr install adjustbox
sudo tlmgr install tcolorbox
sudo tlmgr install collectbox
sudo tlmgr install ucs
sudo tlmgr install environ
sudo tlmgr install trimspaces
sudo tlmgr install titling
sudo tlmgr install enumitem
sudo tlmgr install rsfs

Running the command again:

pandoc demo.md --pdf-engine=xelatex -o demo.pdf

You may see:

[WARNING] Missing character: There is no (U+251C) (U+251C) in font [lmmono10-regular]:!

You can either install a font that supports the symbols, often caused by emojis.

Once the above has been corrected you will be able to generate PDF from Markdown files using:

pandoc demo.md --pdf-engine=xelatex -o demo.pdf

To add a table of contents use the option --toc in the command

Make PDF:

pandoc demo.md --pdf-engine=xelatex --toc -o demo.pdf

Make Epub:

pandoc demo.md --toc -o demo.epub

Front Matter

When working with PDF/Markdown you can specify YAML tags in the markdown to set the book title, author and event the cover image with working with EPUB

This should be at the top of the file, it will not be printed.


---
title: Demo Book
creator:
 - role: author
 text: David Carr
cover-image: cover.jpg
---

Write a book

Here is a basic markdown file; this has chapters designated by # (h1) sub headings can be added by using ## (h2)


---
title: Demo Book
creator:
- role: author
 text: David Carr
cover-image: cover.jpg
---

# Chapter 1

An example chapter.....

# Chapter 2

This is super basic

## Sub chapter that belongs to chapter 2

>Markdown is awesome!

# Chapter 3

Convert this to a PDF or Epub file to produce:

Ebook

]]>
Fri, 24 Jun 2022 09:27:00 GMT https://dcblog.dev/generate-pdf-and-epub-files-using-pandoc https://dcblog.dev/generate-pdf-and-epub-files-using-pandoc
Released Laravel TALL AdminTW theme

Laravel TALL AdminTW is a minimal Livewire theme styled with TailwindCSS.

Laravel AdminTW supports both light and dark mode based on the user's OS.

Provided are blade and Laravel Livewire components for common layout / UI elements and a complete test suite (Pest PHP).

Out of the box AdminTW provides:

  • UUID all ids uses UUID's instead of primary keys
  • Multiple Users with an invite system
  • 2FA user opt in and a system wide 2FA enforcement option
  • Audit Logs - track every action
  • Global Search - search any model
  • Livewire components
  • Blade components
  • Roles & Permissions - set that a user can and cannot do
  • Unit and Feature Tests
  • Dark mode support

Read more at the official website https://laraveladmintw.com/

 

2FA

Add additional security to your account using two-factor authentication.

Why do I need this?

Passwords can get stolen – especially if you use the same password for multiple sites. Adding Two-Step Verification means that even if your password gets stolen, your account will remain secure.

Laravel Admin Two Factor Authentication Setup

Dashboard

The dashboard contains a single card ready to be customised as needed.

Laravel Admin Two Factor Authentication Setup

Audit Trails

Record ever action, then review all actions in the audit trail.

Filter by user, action, type or a date range.

Laravel Admin Two Factor Authentication Setup

Sent Emails

All emails sent are recorded and can be viewed and filtered by To, CC, BCC, Subject and created date range.

Laravel Admin Two Factor Authentication Setup

Settings

From the settings change the following:

  • Application Name
  • Force 2FA for all users
  • Change application logo for light and dark mode
  • Change login logo for light and dark mode
  • Lock application down to set IP addresses

Laravel Admin Two Factor Authentication Setup

Roles

Laravel AdminTW comes with role based permissions.

Set what users can and cannot do.

Assign multiple roles to a user.

Laravel Admin Two Factor Authentication Setup

Users

Laravel AdminTW comes with multiple users support, you can add as many users as needed.

Users can be invited to via an email invitation.

Users have their own profile which shows their activity.

Users can be locked down to set IP addresses. Meaning they would only be able to login from those IP addresses listed in the settings.

Laravel Admin Two Factor Authentication Setup

Tests

Laravel AdminTW comes with a suite of tests using PestPHP.

There are 74 tests out the box that ensured the application works as expected.

Laravel Admin Two Factor Authentication Setup

Read more at the official website https://laraveladmintw.com/

]]>
Tue, 07 Jun 2022 16:38:00 GMT https://dcblog.dev/released-laravel-tall-admintw-theme https://dcblog.dev/released-laravel-tall-admintw-theme
My Termial Aliases

What is a terminal alias?

A terminal alias is a shortcut for a terminal command. For example, as a Laravel user, I would type PHP artisan often so I've created an alias of a so I can then type a route:list to get a list of routes instead of having to type out PHP artisan route:list.

Aliases are a great way to be more efficient at using a terminal for common commands.

How to set up terminal aliases

To create an alias you need to edit a .bash_profile or .profile or event a .bash_aliases or a .zshrc when using ZSH 

When using bash I'll use .bash_profile.

Use VIM to open .bash_profile in the home directory if the file doesn't exist it will be created.

vim ~/.bash_profile

To create an alias use the keyword alias followed by the alias name="" then inside the quotes place the command to be aliased.

For example create an alias called ls that will use the command ls -ls -a which will list all files in a vertical list and show hidden files.

alias ls="ls -ls -a"

My Aliases

The following are the alises I use on my machine.

Tools

Open phpstorm or vscode in the current directory, to open VSCode in a folder navigate to the folder and type code . to open the root folder in VSCode.

#tools
alias storm='open -a "/Users/dave/Applications/JetBrains Toolbox/PhpStorm.app" "`pwd`"'
alias code='open -a "/Applications/Visual Studio Code.app" "`pwd`"'

Running Tests

These will run either pest or PHPUnit depending which if pest is installed. Activate by pressing p.

To run a filter for a specific test use the alias pf followed by the method or file name.

# Run tests
function p() {
   if [ -f vendor/bin/pest ]; then
      vendor/bin/pest "$@"
   else
      vendor/bin/phpunit "$@"
   fi
}

function pf() {
   if [ -f vendor/bin/pest ]; then
      vendor/bin/pest --filter "$@"
   else
      vendor/bin/phpunit --filter "$@"
   fi
}

Laravel

alias a="php artisan"
alias t="clear && php artisan test"
alias tp="clear && php artisan test --parallel"
alias phpunit="vendor/bin/phpunit"
alias pest="vendor/bin/pestphp"

Mac show / hide files

# Show/hide hidden files in Finder
alias show="defaults write com.apple.finder AppleShowAllFiles -bool true && killall Finder"
alias hide="defaults write com.apple.finder AppleShowAllFiles -bool false && killall Finder"

PHP version switch

When PHP is installed via homebrew use these aliases to switch between PHP 8.1 and 7.4

# PHP switcher
alias switch-php8="brew unlink php@7.4 && brew link --overwrite --force php@8.1"
alias switch-php74="brew unlink php && brew link --overwrite --force php@7.4"

Laravel Valet

alias vs='valet secure'
alias tunnel='valet share -subdomain=dc -region=eu'

Edit hosts file

#host file
alias hostfile="sudo vi /etc/hosts"

Composer

#composer
alias cu='composer update'
alias ci='composer install'
alias cda='composer dump-autoload -o'
alias cr='composer require'

GIT

use gac function to GIT Add and Commit files use it like gac . to commit all unstaged files. or use gac a-file-that-changed to commit a specific file.

I alias git to use HUB from GitHub

#git
function gac()
{
   #usage gac . 'the message'
   git add $1 && git commit -m "$2"
}

alias git=hub
alias g="hub"
alias gc="git checkout"
alias gm="git merge"
alias gl="git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
alias gs="git status"
alias gp="git push"
alias gpu="git pull"
alias gno="git reset --hard HEAD"
alias glog='git log --oneline --decorate --graph --all'

IP Lookup

# IP addresses
alias ip="curl https://diagnostic.opendns.com/myip ; echo"
alias localip="ifconfig -a | grep -o 'inet6\? \(addr:\)\?\s\?\(\(\([0-9]\+\.\)\{3\}[0-9]\+\)\|[a-fA-F0-9:]\+\)' | awk '{ sub(/inet6? (addr:)? ?/, \"\"); print }'"

Directory paths

use aliases to make shortcuts for folder locations

#directories
alias ls="ls -ls -a"
alias projects="cd ~/Dropbox\ \(dcblog\)/local/projects"
alias personal="cd ~/Dropbox\ \(dcblog\)/local/personal"

SSH

#ssh
alias sshkey="cat ~/.ssh/id_rsa.pub"
alias sshconfig="vi ~/.ssh/config"
alias copykey='command cat ~/.ssh/id_rsa.public | pbcopy'

Complete collection

#tools
alias storm='open -a "/Users/dave/Applications/JetBrains Toolbox/PhpStorm.app" "`pwd`"'
alias code='open -a "/Applications/Visual Studio Code.app" "`pwd`"'

# Run tests
function p() {
   if [ -f vendor/bin/pest ]; then
      vendor/bin/pest "$@"
   else
      vendor/bin/phpunit "$@"
   fi
}

function pf() {
   if [ -f vendor/bin/pest ]; then
      vendor/bin/pest --filter "$@"
   else
      vendor/bin/phpunit --filter "$@"
   fi
}

#laravel
alias a="php artisan"
alias t="clear && php artisan test"
alias tp="clear && php artisan test --parallel"
alias phpunit="vendor/bin/phpunit"
alias pest="vendor/bin/pestphp"

# Show/hide hidden files in Finder
alias show="defaults write com.apple.finder AppleShowAllFiles -bool true && killall Finder"
alias hide="defaults write com.apple.finder AppleShowAllFiles -bool false && killall Finder"

# PHP switcher
alias switch-php8="brew unlink php@7.4 && brew link --overwrite --force php@8.1"
alias switch-php74="brew unlink php && brew link --overwrite --force php@7.4"

#valet
alias vs='valet secure'
alias tunnel='valet share -subdomain=dc -region=eu'

#host file
alias hostfile="sudo vi /etc/hosts"

#composer
alias cu='composer update'
alias ci='composer install'
alias cda='composer dump-autoload -o'
alias cr='composer require'

#git
function gac()
{
   #usage gac . 'the message'
   git add $1 && git commit -m "$2"
}

alias git=hub
alias g="hub"
alias gc="git checkout"
alias gm="git merge"
alias gl="git log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset' --abbrev-commit"
alias gs="git status"
alias gp="git push"
alias gpu="git pull"
alias gno="git reset --hard HEAD"
alias glog='git log --oneline --decorate --graph --all'

# IP addresses
alias ip="curl https://diagnostic.opendns.com/myip ; echo"
alias localip="ifconfig -a | grep -o 'inet6\? \(addr:\)\?\s\?\(\(\([0-9]\+\.\)\{3\}[0-9]\+\)\|[a-fA-F0-9:]\+\)' | awk '{ sub(/inet6? (addr:)? ?/, \"\"); print }'"

#directories
alias ls="ls -ls -a"
alias projects="cd ~/Dropbox\ \(dcblog\)/local/projects"
alias personal="cd ~/Dropbox\ \(dcblog\)/local/personal"

#ssh
alias sshkey="cat ~/.ssh/id_rsa.pub"
alias sshconfig="vi ~/.ssh/config"
alias copykey='command cat ~/.ssh/id_rsa.public | pbcopy'

Restart the terminal then type your shortcut and press enter to be taken to the aliased location.

Aliases are really useful and simple to set up.

]]>
Sun, 05 Jun 2022 08:44:00 GMT https://dcblog.dev/my-termial-aliases https://dcblog.dev/my-termial-aliases
Mockery 1 Illuminate Console OutputStyle askQuestion() but no expectations were specified

When running a PESTPHP test if you come across this error:

Received Mockery_1_Illuminate_Console_OutputStyle::askQuestion(), but no expectations were specified

Usually caused by caching, clear your cache by running:

php artisan optimize:clear

 

]]>
Sat, 04 Jun 2022 08:46:00 GMT https://dcblog.dev/mockery-1-illuminate-console-outputstyle-askquestion-but-no-expectations-were-specified https://dcblog.dev/mockery-1-illuminate-console-outputstyle-askquestion-but-no-expectations-were-specified
Laravel Security Headers

This weekend, I changed the design of this blog whilst doing so I wanted to add the security headers for content security policies, these tell the application what it can and cannot run, There's a great website called https://securityheaders.com which will scan a URL and tell you what your level is.

If you have no headers set up you'll get an F grade, which is bad! 

Before I started my rating:

an F grade website

Once I finished I have an A rating:

A rating

For detailed information about security headers read Daniel Dušek blog that explains this really well https://danieldusek.com/enabling-security-headers-for-your-website-with-php-and-laravel.html 

Still here? great I'll go over what I've done.

I created a middleware class called SecurityHeaders.php inside App\Http\Middleware of my Laravel application

Add this middleware to the Middleware group inside App\Http\Kernal.php 

protected $middleware = [
    // \App\Http\Middleware\TrustHosts::class,
    \App\Http\Middleware\TrustProxies::class,
    \Fruitcake\Cors\HandleCors::class,
    \App\Http\Middleware\PreventRequestsDuringMaintenance::class,
    \Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
    \App\Http\Middleware\TrimStrings::class,
    \Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
    \App\Http\Middleware\SecurityHeaders::class,
];

Set the headers to be turned off, this provide would be attackers information about the server, you don't need to advertise these to better to turn them off.

private $unwantedHeaders = ['X-Powered-By', 'server', 'Server'];

Referrer Policy

Sets how much information should be sent with requests, in my case I chose the option to not send the referer header for requests to less secure destinations.

$response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');

XSS Protection

Stops loading of pages when they detect reflected cross-site scripting (XSS) attacks

I set: Enables XSS filtering. Rather than sanitizing the page, the browser will prevent rendering of the page if an attack is detected.

$response->headers->set('X-XSS-Protection', '1; mode=block');

Strict Transport Security

informs browsers that the site should only be accessed using HTTPS

$response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');

Content Security Policy

The content security policy sets weather a browser can run JS / CSS on page load.

You will want to either det a URL specifically or a wildcard like *.domain to allow the subdomain of the given domain to run

$response->headers->set('Content-Security-Policy', "default-src 'self'; script-src 'self' platform.twitter.com 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' * data:; font-src 'self' data: ; connect-src 'self'; media-src 'self'; frame-src 'self' platform.twitter.com github.com *.youtube.com *.vimeo.com; object-src 'none'; base-uri 'self'; report-uri ");

Expect-CT

The Expect-CT header lets sites opt in to reporting and/or enforcement of Certificate Transparency requirements

$response->headers->set('Expect-CT', 'enforce, max-age=30');

Permissions-Policy

Sets what permissions the device load the page is given

$response->headers->set('Permissions-Policy', 'autoplay=(self), camera=(), encrypted-media=(self), fullscreen=(), geolocation=(self), gyroscope=(self), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=(self), usb=()');

The complete class:

With this class in place upload your changes to your server and re-run https://securityheaders.com

<?php

namespace App\Http\Middleware;

use Closure;

class SecurityHeaders
{
    private $unwantedHeaders = ['X-Powered-By', 'server', 'Server'];

    /**
     * @param $request
     * @param  Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        $response = $next($request);

        if (!app()->environment('testing')) {
            $response->headers->set('Referrer-Policy', 'no-referrer-when-downgrade');
            $response->headers->set('X-XSS-Protection', '1; mode=block');
            $response->headers->set('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');
            $response->headers->set('Content-Security-Policy', "default-src 'self'; script-src 'self' platform.twitter.com plausible.io utteranc.es *.cloudflare.com 'unsafe-inline' 'unsafe-eval' plausible.io/js/plausible.js utteranc.es/client.js; style-src 'self' *.cloudflare.com 'unsafe-inline'; img-src 'self' * data:; font-src 'self' data: ; connect-src 'self' plausible.io/api/event; media-src 'self'; frame-src 'self' platform.twitter.com plausible.io utteranc.es github.com *.youtube.com *.vimeo.com; object-src 'none'; base-uri 'self';");
            $response->headers->set('Expect-CT', 'enforce, max-age=30');
            $response->headers->set('Permissions-Policy', 'autoplay=(self), camera=(), encrypted-media=(self), fullscreen=(), geolocation=(self), gyroscope=(self), magnetometer=(), microphone=(), midi=(), payment=(), sync-xhr=(self), usb=()');
            $response->headers->set('Access-Control-Allow-Origin', '*');
            $response->headers->set('Access-Control-Allow-Methods', 'GET,POST,PUT,PATCH,DELETE,OPTIONS');
            $response->headers->set('Access-Control-Allow-Headers', 'Content-Type,Authorization,X-Requested-With,X-CSRF-Token');

            $this->removeUnwantedHeaders($this->unwantedHeaders);
        }

        return $response;
    }

    /**
     * @param $headers
     */
    private function removeUnwantedHeaders($headers): void
    {
        foreach ($headers as $header) {
            header_remove($header);
        }
    }
}

 

]]>
Sat, 04 Jun 2022 07:50:00 GMT https://dcblog.dev/laravel-security-headers https://dcblog.dev/laravel-security-headers