Why Your Headlamps and Lanterns Should Always Be Set to This Color Mode Around Camp

https://hips.hearstapps.com/hmg-prod.s3.amazonaws.com/images/headlamps-and-lanterns-color-mode-lead-6555141dcff52.jpg?crop=1.00xw:0.752xh;0,0.0938xh&resize=640:*

Have you ever wondered why headlamps and camp lights come with a red or orange setting? It’s not just for ambience — there’s a surprising benefit to switching from bright white to more mellow red. If you’re sick of dealing with a tent full of flying bugs, this info is for you.

According to the North Carolina State University Agriculture and State Sciences Department: “Most insects have only two types of visual pigments. One pigment absorbs green and yellow light (550 nm); the other absorbs blue and ultraviolet light (<480 nm). Insects cannot see red.” Even if the reasons don’t interest you, the results should. Your camping lantern has a red light mode. Use it, and you won’t attract insects.

I’ve tried this trick myself: At camp I typically use a Biolite Headlamp 325 and Goal Zero Crush Light Chroma, when I’m not testing other lighting solutions. They’re both lightweight, easy to operate and the Goal Zero has solar charging; I leave it on the dash to soak up the sun during the day’s drive, and it’s ready to go when then sun starts to set.

First, I tested the headlamp: Anyone who’s worn a headlamp during the evening at camp is probably familiar with the flickering, flying bugs that are drawn to your face almost immediately after you turn your headlamp on. It’s annoying and can make seeing clearly a chore, cancelling out the appeal of the headlamp in the first place. Sure enough, with the standard white light setting I had bugs up in my grill; once I switched it to red, they all left me alone.

Same with the Crush Light Chroma. We keep one in our GoFast Camper and have built a routine around the red light. When we’re getting into the tent and need to see and have panels open, we set the Chroma to red. No bugs follow us inside, and once we’re in and all the tent flaps are securely shut, we’ll switch to white light. It is trickier to see details at night with red light, so we mostly use if for entering and exiting the tent, and reserve the white light reserved for use after we’ve tucked in for the evening.

Nearly all headlamps come with a red light setting, but read the product description rather than assuming. Camp lights and lanterns aren’t equipped with red light as widely, so check to make sure your pick has the ability to go into red mode. With that in mind, here are a few of my favorites for keeping bugs at bay.

Goal Zero Crush Light Chroma

Black Diamond Spot 400-R Rechargeable Headlamp

Gear Patrol

Simplifying API Integration with Laravel’s Http Facade

https://hashnode.com/utility/r?url=https%3A%2F%2Fcdn.hashnode.com%2Fres%2Fhashnode%2Fimage%2Fupload%2Fv1699585660555%2F4345ca45-14b3-45cd-ac5a-824e7fe8ee60.png%3Fw%3D1200%26h%3D630%26fit%3Dcrop%26crop%3Dentropy%26auto%3Dcompress%2Cformat%26format%3Dwebp%26fm%3Dpng

I’ve been working a lot lately integrating third-party APIs. There are several different approaches to this such as using the third-party provided SDK. However, I feel sticking to Laravel’s Http facade is often a better choice. By using the Http facade, all third-party integrations can have a similar structure, and testing and mocking becomes a lot easier. Also, your application will have fewer dependencies. You won’t have to worry about keeping the SDK up to date or figuring out what to do if the SDK is no longer supported.

In this post, we will explore integrating the Google Books API. I will create a reusable client and request class to make using the API very simple. In future posts, I will go into more detail about testing, mocking, as well as creating API resources.

Let’s get started!

Add Google Books Configuration to Laravel

Now that we have an API key, we can add it to the .env along with the API URL.

GOOGLE_BOOKS_API_URL=https://www.googleapis.com/books/v1
GOOGLE_BOOKS_API_KEY=[API KEY FROM GOOGLE]

For this example, I am storing an API key that I obtained from the Google Cloud console, though it is not needed for the parts of the API we will be accessing. For more advanced API usage, you would need to integrate with Google’s OAuth 2.0 server and create a client ID and secret that could also be stored in the .env file. This is beyond the scope of this post.

With the environment variables in place, open the config/services.php file and add a section for Google Books.

'google_books' => [
    
    'base_url' => env('GOOGLE_BOOKS_API_URL'),
    
    'api_key' => env('GOOGLE_BOOKS_API_KEY'),
],

Create an ApiRequest Class

When making requests to the API, I find it easiest to use a simple class to be able to set any request properties I need.

Below is an example of an ApiRequest class that I use to pass in URL information along with the body, headers, and any query parameters. This class can easily be modified or extended to add additional functionality.

<?php

namespace App\Support;


class ApiRequest
{
    
    protected array $headers = [];

    
    protected array $query = [];

    
    protected array $body = [];

    
    public function __construct(protected HttpMethod $method = HttpMethod::GET, protected string $uri = '')
    {
    }

    
    public function setHeaders(array|string $key, string $value = null): static
    {
        if (is_array($key)) {
            $this->headers = $key;
        } else {
            $this->headers[$key] = $value;
        }

        return $this;
    }

    
    public function clearHeaders(string $key = null): static
    {
        if ($key) {
            unset($this->headers[$key]);
        } else {
            $this->headers = [];
        }

        return $this;
    }

    
    public function setQuery(array|string $key, string $value = null): static
    {
        if (is_array($key)) {
            $this->query = $key;
        } else {
            $this->query[$key] = $value;
        }

        return $this;
    }

    
    public function clearQuery(string $key = null): static
    {
        if ($key) {
            unset($this->query[$key]);
        } else {
            $this->query = [];
        }

        return $this;
    }

    
    public function setBody(array|string $key, string $value = null): static
    {
        if (is_array($key)) {
            $this->body = $key;
        } else {
            $this->body[$key] = $value;
        }

        return $this;
    }

    
    public function clearBody(string $key = null): static
    {
        if ($key) {
            unset($this->body[$key]);
        } else {
            $this->body = [];
        }

        return $this;
    }

    
    public function getHeaders(): array
    {
        return $this->headers;
    }

    
    public function getQuery(): array
    {
        return $this->query;
    }

    
    public function getBody(): array
    {
        return $this->body;
    }

    
    public function getUri(): string
    {
        if (empty($this->query) || $this->method === HttpMethod::GET) {
            return $this->uri;
        }

        return $this->uri.'?'.http_build_query($this->query);
    }

    
    public function getMethod(): HttpMethod
    {
        return $this->method;
    }

    
    

    public static function get(string $uri = ''): static
    {
        return new static(HttpMethod::GET, $uri);
    }

    public static function post(string $uri = ''): static
    {
        return new static(HttpMethod::POST, $uri);
    }

    public static function put(string $uri = ''): static
    {
        return new static(HttpMethod::PUT, $uri);
    }

    public static function delete(string $uri = ''): static
    {
        return new static(HttpMethod::DELETE, $uri);
    }
}

The class constructor takes an HttpMethod, which is just a simple enum with the various HTTP methods, and a URI.

enum HttpMethod: string
{
    case GET = 'get';
    case POST = 'post';
    case PUT = 'put';
    case DELETE = 'delete';
}

There are helper methods to create the request using the HTTP method name and passing a URI. Finally, there are methods to add and clear headers, query parameters, and body data.

Create an API Client

Now that we have the request, we need an API client to send it. This is where we can use the Http facade.

Abstract ApiClient

First, we’ll create an abstract ApiClient class that will be extended by our various APIs.

<?php

namespace App\Support;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Response;
use Illuminate\Support\Facades\Http;


abstract class ApiClient
{
    
    public function send(ApiRequest $request): Response
    {
        return $this->getBaseRequest()
            ->withHeaders($request->getHeaders())
            ->{$request->getMethod()->value}(
                $request->getUri(),
                $request->getMethod() === HttpMethod::GET
                    ? $request->getQuery()
                    : $request->getBody()
            );
    }

    
    protected function getBaseRequest(): PendingRequest
    {
        $request = Http::acceptJson()
            ->contentType('application/json')
            ->throw()
            ->baseUrl($this->baseUrl());

        return $this->authorize($request);
    }

    
    protected function authorize(PendingRequest $request): PendingRequest
    {
        return $request;
    }

    
    abstract protected function baseUrl(): string;
}

This class has a getBaseRequest method that sets up some sane defaults using the Http facade to create a PendingRequest. It calls the authorize method which we can override in our Google Books implementation to set our API key.

The baseUrl method is just a simple abstract method that our Google Books class will set to use the Google Books API URL we set earlier.

Finally, the send method is what sends the request to the API. It takes an ApiRequest parameter to build up the request, then returns the response.

GoogleBooksApiClient

With the abstract client created, we can now create a GoogleBooksApiClient to extend it.

<?php

namespace App\Support;

use Illuminate\Http\Client\PendingRequest;


class GoogleBooksApiClient extends ApiClient
{
    
    protected function baseUrl(): string
    {
        return config('services.google_books.base_url');
    }

    
    protected function authorize(PendingRequest $request): PendingRequest
    {
        return $request->withQueryParameters([
            'key' => config('services.google_books.api_key'),
        ]);
    }
}

In this class, we just need to set the base URL and configure the authorization. For the Google Books API, that means passing the API key as a URL parameter and setting an empty Authorization header.

If we had an API that used a bearer authorization, we could have an authorize method like the following:

protected function authorize(PendingRequest $request): PendingRequest
{
    return $request->withToken(config(services.someApi.token));
}

The nice part about having this authorize method is the flexibility it offers to support a variety of API authorization methods.

Query Books By Title

Now that we have our ApiRequest class and GoogleBooksApiClient, we can create an action to query books by title. It would look something like this:

<?php

namespace App\Actions;

use App\Support\ApiRequest;
use App\Support\GoogleBooksApiClient;
use Illuminate\Http\Client\Response;


class QueryBooksByTitle
{
    
    public function __invoke(string $title): Response
    {
        $client = app(GoogleBooksApiClient::class);

        $request = ApiRequest::get('volumes')
            ->setQuery('q', 'intitle:'.$title)
            ->setQuery('printType', 'books');

        return $client->send($request);
    }
}

Then, to call the action, if I wanted to find information about the book The Ferryman, which I just read and highly recommend, use the following snippet:

use App\Actions\QueryBooksByTitle;

$response = app(QueryBooksByTitle::class)("The Ferryman");

$response->json();

Bonus: Tests

Below, I added some examples for testing the request and client classes. For the tests, I am using Pest PHP which provides a clean syntax and additional features on top of PHPUnit.

ApiRequest

<?php

use App\Support\ApiRequest;
use App\Support\HttpMethod;

it('sets request data properly', function () {
    $request = (new ApiRequest(HttpMethod::GET, '/'))
        ->setHeaders(['foo' => 'bar'])
        ->setQuery(['baz' => 'qux'])
        ->setBody(['quux' => 'quuz']);

    expect($request)
        ->getHeaders()->toBe(['foo' => 'bar'])
        ->getQuery()->toBe(['baz' => 'qux'])
        ->getBody()->toBe(['quux' => 'quuz'])
        ->getMethod()->toBe(HttpMethod::GET)
        ->getUri()->toBe('/');
});

it('sets request data properly with a key->value', function () {
    $request = (new ApiRequest(HttpMethod::GET, '/'))
        ->setHeaders('foo', 'bar')
        ->setQuery('baz', 'qux')
        ->setBody('quux', 'quuz');

    expect($request)
        ->getHeaders()->toBe(['foo' => 'bar'])
        ->getQuery()->toBe(['baz' => 'qux'])
        ->getBody()->toBe(['quux' => 'quuz'])
        ->getMethod()->toBe(HttpMethod::GET)
        ->getUri()->toBe('/');
});

it('clears request data properly', function () {
    $request = (new ApiRequest(HttpMethod::GET, '/'))
        ->setHeaders(['foo' => 'bar'])
        ->setQuery(['baz' => 'qux'])
        ->setBody(['quux' => 'quuz']);

    $request->clearHeaders()
        ->clearQuery()
        ->clearBody();

    expect($request)
        ->getHeaders()->toBe([])
        ->getQuery()->toBe([])
        ->getBody()->toBe([])
        ->getUri()->toBe('/');
});

it('clears request data properly with a key', function () {
    $request = (new ApiRequest(HttpMethod::GET, '/'))
        ->setHeaders('foo', 'bar')
        ->setQuery('baz', 'qux')
        ->setBody('quux', 'quuz');

    $request->clearHeaders('foo')
        ->clearQuery('baz')
        ->clearBody('quux');

    expect($request)
        ->getHeaders()->toBe([])
        ->getQuery()->toBe([])
        ->getBody()->toBe([])
        ->getUri()->toBe('/');
});

it('creates instance with correct method', function (HttpMethod $method) {
    $request = ApiRequest::{$method->value}('/');

    expect($request->getMethod())->toBe($method);
})->with([
    [HttpMethod::GET],
    [HttpMethod::POST],
    [HttpMethod::PUT],
    [HttpMethod::DELETE],
]);

The ApiRequest tests check that the correct request data is being set and the correct methods are being used.

ApiClient

Testing for the ApiClient will be a little more complex. Since it is an abstract class, we will use an anonymous class in the beforeEach function to create a client to use that extends ApiClient.

Notice, that we also use the Http::fake() method. This creates mocks on the Http facade that we can make assertions against and prevent making API requests in the tests.

<?php

use App\Support\ApiClient;
use App\Support\ApiRequest;
use App\Support\HttpMethod;
use Illuminate\Http\Client\PendingRequest;
use Illuminate\Http\Client\Request;
use Illuminate\Support\Facades\Http;

beforeEach(function () {
    Http::fake();

    $this->client = new class extends ApiClient
    {
        protected function baseUrl(): string
        {
            return 'https://example.com';
        }
    };
});

it('sends a get request', function () {
    $request = ApiRequest::get('foo')
        ->setHeaders(['X-Foo' => 'Bar'])
        ->setQuery(['baz' => 'qux']);

    $this->client->send($request);

    Http::assertSent(static function (Request $request) {
        expect($request)
            ->url()->toBe('https://example.com/foo?baz=qux')
            ->method()->toBe(HttpMethod::GET->name)
            ->header('X-Foo')->toBe(['Bar']);

        return true;
    });
});

it('sends a post request', function () {
    $request = ApiRequest::post('foo')
        ->setBody(['foo' => 'bar'])
        ->setHeaders(['X-Foo' => 'Bar'])
        ->setQuery(['baz' => 'qux']);

    $this->client->send($request);

    Http::assertSent(static function (Request $request) {
        expect($request)
            ->url()->toBe('https://example.com/foo?baz=qux')
            ->method()->toBe(HttpMethod::POST->name)
            ->data()->toBe(['foo' => 'bar'])
            ->header('X-Foo')->toBe(['Bar']);

        return true;
    });
});

it('sends a put request', function () {
    $request = ApiRequest::put('foo')
        ->setBody(['foo' => 'bar'])
        ->setHeaders(['X-Foo' => 'Bar'])
        ->setQuery(['baz' => 'qux']);

    $this->client->send($request);

    Http::assertSent(static function (Request $request) {
        expect($request)
            ->url()->toBe('https://example.com/foo?baz=qux')
            ->method()->toBe(HttpMethod::PUT->name)
            ->data()->toBe(['foo' => 'bar'])
            ->header('X-Foo')->toBe(['Bar']);

        return true;
    });
});

it('sends a delete request', function () {
    $request = ApiRequest::delete('foo')
        ->setBody(['foo' => 'bar'])
        ->setHeaders(['X-Foo' => 'Bar'])
        ->setQuery(['baz' => 'qux']);

    $this->client->send($request);

    Http::assertSent(static function (Request $request) {
        expect($request)
            ->url()->toBe('https://example.com/foo?baz=qux')
            ->method()->toBe(HttpMethod::DELETE->name)
            ->data()->toBe(['foo' => 'bar'])
            ->header('X-Foo')->toBe(['Bar']);

        return true;
    });
});

it('handles authorization', function () {
    $client = new class extends ApiClient
    {
        protected function baseUrl(): string
        {
            return 'https://example.com';
        }

        protected function authorize(PendingRequest $request): PendingRequest
        {
            return $request->withHeaders(['Authorization' => 'Bearer foo']);
        }
    };

    $request = ApiRequest::get('foo');

    $client->send($request);

    Http::assertSent(static function (Request $request) {
        expect($request)->header('Authorization')->toBe(['Bearer foo']);

        return true;
    });
});

For the tests, we are confirming that the request properties are being set correctly on the various request methods. We also confirm the baseUrl and authorize methods are being called correctly. To make these assertions, we are using the Http::assertSent method which expects a callback with a $request that we can test against. Notice that I am using the PestPHP expectations and then returning true. We could just use a normal comparison and return that, but by using the expectations, we get much cleaner error messages when the tests fail. Read this excellent article for more information.

GoogleBooksApiClientTest

The test for the GoogleBooksApiClient is similar to the ApiClient test where we just want to make sure our custom implementation details are being handled properly, like setting the base URL and adding a query parameter with the API key.

Also, not the config helper in the beforeEach method. By using the helper, we can set test values for the Google Books service config that will be used in each of our tests.

<?php

use App\Support\ApiRequest;
use App\Support\GoogleBooksApiClient;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Client\Request;

beforeEach(function () {
    Http::fake();
    config([
        'services.google_books.base_url' => 'https://example.com',
        'services.google_books.api_key' => 'foo',
    ]);
});

it('sets the base url', function () {
    $request = ApiRequest::get('foo');

    app(GoogleBooksApiClient::class)->send($request);

    Http::assertSent(static function (Request $request) {
        expect($request)->url()->toStartWith('https://example.com/foo');

        return true;
    });
});

it('sets the api key as a query parameter', function () {
    $request = ApiRequest::get('foo');

    app(GoogleBooksApiClient::class)->send($request);

    Http::assertSent(static function (Request $request) {
        expect($request)->url()->toContain('key=foo');

        return true;
    });
});

Summary

In this article, we covered some helpful steps for integrating third-party APIs in Laravel. By using these simple custom classes, along with the Http facade, we can ensure all integrations function similarly, are easier to test, and don’t require any project dependencies. In a later post, I will expand on these integration tips by covering DTOs, testing with mock responses, and using API resources.

Thanks for reading!

Laravel News Links

xTool Laser Screenprinting System

https://theawesomer.com/photos/2023/11/xtool_laser_screenprinting_system_t.jpg

xTool Laser Screenprinting System

 | Pledge

Screenprinting involves coating screens with chemicals, exposing the areas you want to print to light, then washing them to allow ink to flow through. xTool’s screenprinting system uses a laser to expose pre-coated screens for crisp screens in hours, not days. Price shown includes an xTool 5-watt laser. A $200 basic kit is available for other laser engravers.

The Awesomer

You Can Get Almost-New Camera Gear for a Fraction of the Cost. Here’s How.

https://cdn.thewirecutter.com/wp-content/media/2023/11/buyingusedcameragear-2048px-mirrorlesscameras-3×2-1.jpg?auto=webp&quality=60&width=630&dpr=2

Four cameras shown side by side on top of a wooden fence.

Let’s be real—camera gear is expensive. And the hit to your wallet can feel most acute as you’re starting to build a system from scratch, when a new entry-level camera costs you north of $500, and it may not even come with a lens. Buying used, however, can get you 90% of the quality and longevity of new gear at 60% of the price. That’s a deal that’s hard to turn down.

Dismiss

Wirecutter: Reviews for the Real World

Turbine UI – Laravel Blade & Tailwind CSS UI Component Library

https://picperf.io/https://laravelnews.s3.amazonaws.com/featured-images/turbine-ui.jpg

Turbine UI - Laravel Blade & Tailwind CSS UI Component Library

Turbine UI is a library of web components built for Laravel Blade and styled with Tailwind CSS. Turbine UI has been designed with simplicity and flexibility in mind, making it easy for you to style your apps and features over 30 professionally designed components.

You can effortlessly deploy these components out of the box or tailor them to perfection using the proprietary theme and variant systems.

Building a Laravel front-end with Turbine UI transforms the design process into a seamless joy, ensuring maintainability is a breeze.

You can get started for free by installing Turbine UI via Composer:

composer require brandymedia/turbine-ui-core

Also, check out the home page and official documentation for more details.


The post Turbine UI – Laravel Blade & Tailwind CSS UI Component Library appeared first on Laravel News.

Join the Laravel Newsletter to get all the latest Laravel articles like this directly in your inbox.

Laravel News

Cheap Gun Opportunity at Houston Gun Buyback Event

https://www.ammoland.com/wp-content/uploads/2023/11/Houston-gun-turn-in-buyback-Nov-18-2023-1000-500×367.jpg

Cheap Gun Opportunity at Houston Gun Buyback Event
Cheap Gun Opportunity at Houston Gun Buyback Event

On Saturday, 18 November, the city of Houston will host a gun buyback event. The event will be held at Westchase Park and Ride, 11050 Harwin Drive, Houston, from 8 a.m. to Noon. No questions will be asked of people turning in guns for gift cards. The event is funded with American Rescue Plan money, part of the 1.6 trillion dollar Biden Administration spending, which is driving inflation.

  • $50 will be offered for guns that do not work
  • $100 for working rifles or shotguns
  • $150 for working handguns
  • $200 for semi-automatic rifles
  • No “Ghost Guns” or pellet guns will be accepted.

Houston has already had several of these gun turn-in events, labeled with the Orwellian term “buyback.” As Houston did not own these firearms previously, they cannot buy them back.

In a previous Houston event, on July 30, 2022, an activist turned in a box full of 3D-printed crude, single-shot pistols and walked away with a significant chunk of change. The upcoming event on November 18 will not pay for “ghost guns.”

The Biden administration claims “ghost guns” are excessively dangerous. In a brief to the Supreme Court, the Biden administration makes this claim:

Ghost guns can be made from kits and parts that are available online to anyone with a credit card and that allow anyone with basic tools and rudimentary skills (or access to Internet video tutorials) to assemble a fully functional firearm in as little as twenty minutes. Some manufacturers of those kits and parts assert that they are not “firearms” regulated by federal law, and thus can be sold without serial numbers, transfer records, or background checks. Those features of ghost guns make them uniquely attractive to criminals and others who are legally prohibited from buying firearms.

The problem with accepting “ghost guns” at the gun turn-in scheduled for Houston is that “ghost guns” quickly transfer gift cards from the event to people who are willing to spend a few dollars on making “ghost guns.”  Accepting “ghost guns” undercuts the propaganda value of the event.  Not accepting “ghost guns” undercuts “ghost gun” propaganda.

In previous Houston gun turn-in events, many private buyers have found some excellent deals for good guns.

When the event only offers $100 for working rifles or shotguns, a nice old double barrel might be picked up for $120. Maybe someone will have Grandpa’s old Browning Auto-5 for $150. A purchase of $175 might bag a Smith & Wesson model 10 in good condition. At the recent gun turn-in at Dallas, one participant procured a minty Colt, pre-ban SP-1, a desirable collector item worth $2,000 to $3,000 for $400.

Even well-funded events, such as in Houston, usually run out of gift cards before the event ends. Private buyers often make very good deals after the gift cards run out.  Private purchase of firearms is legal in most states, as it is in Texas.

The right to purchase firearms without government permission is a basic part of the Second Amendment.

The Heller decision mentioned regulation of the commercial sale of firearms might be accepted under the Second Amendment. This implies private sales of firearms should not be regulated. The private sale of firearms acts as a check against creating a firearms registry. The only serious purpose of a firearms registry is to facilitate the confiscation of private arms, whether wholesale or piecemeal.

Academic studies have found gun turn-in events do not reduce homicides or suicides. They may do some harm with a slight uptick in gun crimes following these events.

Lines for these events start early. Those who come first are the most likely to obtain gift cards before they run out. This offers an opportunity to private purchasers to talk to people in line to see what they have.

There are certain to be private purchasers at the Houston turn-in event. This correspondent expects to publish a report after the event has ended.


About Dean Weingarten:

Dean Weingarten has been a peace officer, a military officer, was on the University of Wisconsin Pistol Team for four years, and was first certified to teach firearms safety in 1973. He taught the Arizona concealed carry course for fifteen years until the goal of Constitutional Carry was attained. He has degrees in meteorology and mining engineering, and retired from the Department of Defense after a 30 year career in Army Research, Development, Testing, and Evaluation.

Dean Weingarten

AmmoLand Shooting Sports News

My Hanukkah wish list

https://GunFreeZone.net/wp-content/uploads/2023/11/Screenshot_20231114_100341_Amazon-Shopping.jpg

I’ve been watching all these videos of people mobbing cars and harassing Jews, and it reminds me that you need to check your tire pressure this time of year because the cold weather can make your tires run low.

I found this on Amazon and it looks like it might be useful to have.

 

Like everyone knows, there’s nothing like a nice piece of hickory.

Gun Free Zone