API Integrations using Saloon in Laravel

https://laravelnews.imgix.net/images/saloon-featured.png?ixlib=php-3.3.1

We have all been there, we want to integrate with a 3rd party API in Laravel and we ask ourselves “How should I do this?”. When it comes to API integrations I am no stranger, but still each time I wonder what is going to be the best way. Sam Carré built a package early in 2022 called Saloon that can make our API integrations amazing. This article however is going to be very different, this is going to be a walk through on how you can use it to build an integration, from scratch.

Like all great things it starts with a laravel new and goes from there, so let’s get started. Now when it comes to installing Laravel you can use the laravel installer or composer – that part is up to you. I would recommend the installer if you can though, as it provides easy options to do more than just create a project. Create a new project and open it in your code editor of choice. Once we are there, we can get started.

What are we going to build? I am glad you asked! We are going to be building an integration with the GitHub API to get a list of workflows available for a repo. Now this could be super helpful if you, like me, spend a lot of time in the command line. You are working on an app, you push changes to a branch or create a PR – it goes though a workflow that could be running one of many other things. Knowing the status of this workflow sometimes has a huge impact on what you do next. Is that feature complete? Were there issues with our workflow run? Are our tests or static analysis passing? All of these things you would usually wait and check the repo on GitHub to see the status. This integration will allow you to run an artisan command, get a list of available workflows for a repo, and allow you to trigger a new workflow run.

So by now, composer should have done its thing and installed the perfect starting point, a Laravel application. Next we need to install Saloon – but we want to make sure that we install the laravel version, so run the following inside your terminal:

1composer require sammyjo20/saloon-laravel

Just like that, we are a step closer to easier integrations already. If you have any issues at this stage, make sure that you check both the Laravel and PHP versions you are using, as Saloon requires at least Laravel 8 and PHP 8!

So, now we have Saloon installed we need to create a new class. In Saloons terminology these are “Connectors” and all a connector does is create an object focused way to say – this API is connected through this class. There is a handy artisan command that allows you to create these, so run the following artisan command to create a GitHub connector:

1php artisan saloon:connector GitHub GitHubConnector

This command is split into 2 parts, the first argument is the Integration you are creating and the second is the name of the connector you want to create. This means that you can create multiple connectors for an integration – which gives you a lot of control to connect in many different ways should you need to.

This will have created a new class for you under app/Http/Integrations/GitHub/GitHubConnector.php, let’s have a look at this a moment, and understand what is going on.

The first thing we see is that our connector extends the SaloonConnector, which is what will allow us to get our connector working without a lot of boilerplate code. Then we inherit a trait called AcceptsJson. Now if we look at the Saloon documentation, we know that this is a plugin. This basically adds a header to our requests telling the 3rd party API that we want to Accept JSON responses. The next thing we see is that we have a method for defining the base URL for our connector – so let’s add ours in:

1public function defineBaseUrl(): string

2{

3 return 'https://api.github.com';

4}

Nice and clean, we could even take this a little further so we are dealing with less loose strings hanging around in our application – so let’s look at how we can do that. Inside your config/services.php file add a new service record:

1'github' => [

2 'url' => env('GITHUB_API_URL', 'https://api.github.com'),

3]

What this will do is allow us to override this in different environments – giving us a better and more testable solution. Locally we could even mock the GitHub API using their OpenAPI specification, and test against that to ensure that it works. However, this tutorial is about Saloon so I digress… Now let us refactor our base URL method to use the configuration:

1public function defineBaseUrl(): string

2{

3 return (string) config('services.github.url');

4}

As you can see we are now fetching the newly added record from our configuration – and casting it to a string for type safety – config() returns a mixed result so we want to be strict on this if we can.

Next we have default headers and default config, now right now I am not going to worry about the default headers, as we will approach auth on it’s own in a little while. But the configuration is where we can define the guzzle options for our integration, as Saloon uses Guzzle under the hood. For now let’s set the timeout and move on, but feel free to spend some time configuring this as you see fit:

1public function defaultConfig(): array

2{

3 return [

4 'timeout' => 30,

5 ];

6}

We now have our Connector as configured as we need it for now, we can come back later if we find something we need to add. The next step is to start thinking about the requests we want to be sending. If we look at the API documentation for GitHub Actions API we have many options, we will start with listing the workflows for a particular repository: /repos/{owner}/{repo}/actions/workflows. Run the following artisan command the create a new request:

1php artisan saloon:request GitHub ListRepositoryWorkflowsRequest

Again the first argument is the Integration, and the second argument is the name of the request we want to create. We need to make sure we name the integration for the request we are creating so it lives in the right place, then we need to give it a name. I called mine ListRepositoryWorkflowsRequest because I like a descriptive naming approach – however, feel free to adapt this to how you like to name things, as there is no real wrong way here. This will have created a new file for us to look at: app/Http/Integrations/GitHub/Requests/ListRepositoryWorkflowsRequest.php – let us have a look at this now.

Again we are extending a library class here, this time the SaloonRequest which is to be expected. We then have a connector property and a method. We can change the method if we need to – but the default GET is what we need right now. Then we have a method for defining the endpoint. Refactor your request class to look like the below example:

1class ListRepositoryWorkflowsRequest extends SaloonRequest

2{

3 protected ?string $connector = GitHubConnector::class;

4 

5 protected ?string $method = Saloon::GET;

6 

7 public function __construct(

8 public string $owner,

9 public string $repo,

10 ) {}

11 

12 public function defineEndpoint(): string

13 {

14 return "/repos/{$this->owner}/{$this->repo}/actions/workflows";

15 }

16}

What we have done is add a constructor which accepts the repo and owner as arguments which we can then use within our define endpoint method. We have also set the connector to the GitHubConnector we created earler. So we have a request we know we can send, we can take a small step away from the integration and think about the Console Command instead.

If you haven’t created a console command in Laravel before, make sure you check out the documentation which is very good. Run the following artisan command to create the first command for this integration:

1php artisan make:command GitHub/ListRepositoryWorkflows

This will have created the following file: app/Console/Commands/GitHub/ListRespositoryWorkflows.php. We can now start working with our command to make this send the request and get the data we care about. The first thing I always do when it comes to console commands, is think on the signature. How do I want this to be called? It needs to be something that explains what it is doing, but it also needs to be memorable. I am going to call mine github:workflows as it explains it quite well to me. We can also add a description to our console command, so that when browsing available commands it explains the purpose better: “Fetch a list of workflows from GitHub by the repository name.”

Finally we get to the handle method of our command, the part where we actualy need to do something. In our case we are going to be sending a request, getting some data and displaying that data in some way. However before we can do that, there is one thing we have not done up until this point. That is Authentication. With every API integration, Authentication is one of the key aspects – we need the API to know not only who we are but also that we are actually allowed to make this request. If you go to your GitHub settings and click through to developer settings and personal access tokens, you will be able to generate your own here. I would recommand using this approach instead of going for a full OAuth application for this. We do not need OAuth we just need users to be able to access what they need.

Once you have your access token, we need to add it to our .env file and make sure we can pull it through our configuration.

1GITHUB_API_TOKEN=ghp_loads-of-letters-and-numbers-here

We can now extends our service in config/services.php under github to add this token:

1'github' => [

2 'url' => env('GITHUB_API_URL', 'https://api.github.com'),

3 'token' => env('GITHUB_API_TOKEN'),

4]

Now we have a good way of loading this token in, we can get back to our console command! We need to ammend our signature to allow us to accept the owner and repository as arguments:

1class ListRepositoryWorkflows extends Command

2{

3 protected $signature = 'github:workflows

4 {owner : The owner or organisation.}

5 {repo : The repository we are looking at.}

6 ';

7 

8 protected $description = 'Fetch a list of workflows from GitHub by the repository name.';

9 

10 public function handle(): int

11 {

12 return 0;

13 }

14}

Now we can turn our focus onto the handle method:

1public function handle(): int

2{

3 $request = new ListRepositoryWorkflowsRequest(

4 owner: $this->argument('owner'),

5 repo: $this->argument('repo'),

6 );

7 

8 return self::SUCCESS;

9}

Here we are starting to build up our request by passing the arguments straight into the Request itself, however what we might want to do is create some local variables to provide some console feedback:

1public function handle(): int

2{

3 $owner = (string) $this->argument('owner');

4 $repo = (string) $this->argument('repo');

5 

6 $request = new ListRepositoryWorkflowsRequest(

7 owner: $owner,

8 repo: $repo,

9 );

10 

11 $this->info(

12 string: "Fetching workflows for {$owner}/{$repo}",

13 );

14 

15 return self::SUCCESS;

16}

So we have some feedback to the user, which is always important when it comes to a console command. Now we need to add our authentication token and actually send the request:

1public function handle(): int

2{

3 $owner = (string) $this->argument('owner');

4 $repo = (string) $this->argument('repo');

5 

6 $request = new ListRepositoryWorkflowsRequest(

7 owner: $owner,

8 repo: $repo,

9 );

10 

11 $request->withTokenAuth(

12 token: (string) config('services.github.token'),

13 );

14 

15 $this->info(

16 string: "Fetching workflows for {$owner}/{$repo}",

17 );

18 

19 $response = $request->send();

20 

21 return self::SUCCESS;

22}

If you ammend the above and do a dd() on $response->json(), just for now. Then run the command:

1php artisan github:workflows laravel laravel

This will get a list of workflows for the laravel/laravel repo. Our command will allow you to work with any public repos, if you wanted this to be more specific you could build up an option list of repos you want to check against instead of accepting arguments – but that part is up to you. For this tutorial I am going to focus on the wider more open use case.

Now the response we get back from the GitHub API is great and informative, but it will require transforming for display, and if we look at it in isolation, there is no context. Instead we will add another plugin to our request, which will allow us to transform responses into DTOs (Domain Transfer Objects) which is a great way to handle this. It will allow us to loose the flexible array we are used to getting from APIs, and get something that is more contextually aware. Let’s create a DTO for a Workflow, create a new file: app/Http/Integrations/GitHub/DataObjects/Workflow.php and add the follow code to it:

1class Workflow

2{

3 public function __construct(

4 public int $id,

5 public string $name,

6 public string $state,

7 ) {}

8 

9 public static function fromSaloon(array $workflow): static

10 {

11 return new static(

12 id: intval(data_get($workflow, 'id')),

13 name: strval(data_get($workflow, 'name')),

14 state: strval(data_get($workflow, 'state')),

15 );

16 }

17 

18 public function toArray(): array

19 {

20 return [

21 'id' => $this->id,

22 'name' => $this->name,

23 'state' => $this->state,

24 ];

25 }

26}

We have a constructor which contains the important parts of our workflow that we want to display, a fromSaloon method which will transform an array from a saloon response into a new DTO, and a to array method for displaying the DTO back to an array when we need it. Inside our ListRepositoryWorkflowsRequest we need to inherit a new trait and add a new method:

1class ListRepositoryWorkflowsRequest extends SaloonRequest

2{

3 use CastsToDto;

4 

5 protected ?string $connector = GitHubConnector::class;

6 

7 protected ?string $method = Saloon::GET;

8 

9 public function __construct(

10 public string $owner,

11 public string $repo,

12 ) {}

13 

14 public function defineEndpoint(): string

15 {

16 return "/repos/{$this->owner}/{$this->repo}/actions/workflows";

17 }

18 

19 protected function castToDto(SaloonResponse $response): Collection

20 {

21 return (new Collection(

22 items: $response->json('workflows'),

23 ))->map(function ($workflow): Workflow {

24 return Workflow::fromSaloon(

25 workflow: $workflow,

26 );

27 });

28 }

29}

We inherit the CastsToDto trait, which allows this request to call the dto method on a response, and then we add a castToDto method where we can control how this is transformed. We want this to return a new Collection as there is more than one workflow, using the workflows part of the response body. We then map over each item in the collection – and turn it into a DTO. Now we can either do it this way, or we can do it this way where we build our collection with DTOs:

1protected function castToDto(SaloonResponse $response): Collection

2{

3 return new Collection(

4 items: $response->collect('workflows')->map(fn ($workflow) =>

5 Workflow::fromSaloon(

6 workflow: $workflow

7 ),

8 )

9 );

10}

You can choose what works best for you here. I prefer the first approach personally as I like to step through and see the logic, but there is nothing wrong with either approach – the choice is yours. Back to the command now, we now need to think about how we want to be displaying this information:

1public function handle(): int

2{

3 $owner = (string) $this->argument('owner');

4 $repo = (string) $this->argument('repo');

5 

6 $request = new ListRepositoryWorkflowsRequest(

7 owner: $owner,

8 repo: $repo,

9 );

10 

11 $request->withTokenAuth(

12 token: (string) config('services.github.token'),

13 );

14 

15 $this->info(

16 string: "Fetching workflows for {$owner}/{$repo}",

17 );

18 

19 $response = $request->send();

20 

21 if ($response->failed()) {

22 throw $response->toException();

23 }

24 

25 $this->table(

26 headers: ['ID', 'Name', 'State'],

27 rows: $response

28 ->dto()

29 ->map(fn (Workflow $workflow) =>

30 $workflow->toArray()

31 )->toArray(),

32 );

33 

34 return self::SUCCESS;

35}

So we create a table, with the headers, then for the rows we want the response DTO and we will map over the collection returned, casting each DTO back to an array to be displayed. This may seem counter intuative to cast from a response array to a DTO and back to an array, but what this will do is enforce types so that the ID, name and status are always there when expected and it won’t give any funny results. It allows consistency where a normal response array may not have it, and if we wanted to we could turn this into a Value Object where we have behaviour attached instead. If we now run our command we should now see a nice table output which is easier to read than a few lines of strings:

1php artisan github:workflows laravel laravel

1Fetching workflows for laravel/laravel

2+----------+------------------+--------+

3| ID | Name | State |

4+----------+------------------+--------+

5| 12345678 | pull requests | active |

6| 87654321 | Tests | active |

7| 18273645 | update changelog | active |

8+----------+------------------+--------+

Lastly, just listing out these workflow is great – but let’s take it one step further in the name of science. Let’s say you were running this command against one of your repos, and you wanted to run the update changelog manaually? Or maybe you wanted this to be triggered on a cron using your live production server or any event you might think of? We could set the changelog to run once a day at midnight so we get daily recaps in the changelog or anything we might want. Let us create another console command to create a new workflow dispatch event:

1php artisan saloon:request GitHub CreateWorkflowDispatchEventRequest

Inside of this new file app/Http/Integrations/GitHub/Requests/CreateWorkflowDispatchEventRequest.php add the following code so we can walk through it:

1class CreateWorkflowDispatchEventRequest extends SaloonRequest

2{

3 use HasJsonBody;

4 

5 protected ?string $connector = GitHubConnector::class;

6 

7 public function defaultData(): array

8 {

9 return [

10 'ref' => 'main',

11 ];

12 }

13 

14 protected ?string $method = Saloon::POST;

15 

16 public function __construct(

17 public string $owner,

18 public string $repo,

19 public string $workflow,

20 ) {}

21 

22 public function defineEndpoint(): string

23 {

24 return "/repos/{$this->owner}/{$this->repo}/actions/workflows/{$this->workflow}/dispatches";

25 }

26}

We are setting the connector, and inheriting the HasJsonBody trait to allow us to send data. The method has been set to be a POST request as we want to send data. Then we have a constructor which accepts the parts of the URL that builds up the endpoint. Finally we have dome default data inside defaultData which we can use to set defaults for this post request. As it is for a repo, we can pass either a commit hash or a branch name here – so I have set my default to main as that is what I usually call my production branch. We can now trigger this endpoint to dispatch a new workflow event, so let us create a console command to control this so we can run it from our CLI:

1php artisan make:command GitHub/CreateWorkflowDispatchEvent

Now let’s fill in the details and then we can walk through what is happening:

1class CreateWorkflowDispatchEvent extends Command

2{

3 protected $signature = 'github:dispatch

4 {owner : The owner or organisation.}

5 {repo : The repository we are looking at.}

6 {workflow : The ID of the workflow we want to dispatch.}

7 {branch? : Optional: The branch name to run the workflow against.}

8 ';

9 

10 protected $description = 'Create a new workflow dispatch event for a repository.';

11 

12 public function handle(): int

13 {

14 $owner = (string) $this->argument('owner');

15 $repo = (string) $this->argument('repo');

16 $workflow = (string) $this->argument('workflow');

17 

18 $request = new CreateWorkflowDispatchEventRequest(

19 owner: $owner,

20 repo: $repo,

21 workflow: $workflow,

22 );

23 

24 $request->withTokenAuth(

25 token: (string) config('services.github.token'),

26 );

27 

28 if ($this->hasArgument('branch')) {

29 $request->setData(

30 data: ['ref' => $this->argument('branch')],

31 );

32 }

33 

34 $this->info(

35 string: "Requesting a new workflow dispatch for {$owner}/{$repo} using workflow: {$workflow}",

36 );

37 

38 $response = $request->send();

39 

40 if ($response->failed()) {

41 throw $response->toException();

42 }

43 

44 $this->info(

45 string: 'Request was accepted by GitHub',

46 );

47 

48 return self::SUCCESS;

49 }

50}

So like before we have a signature and a description, our signature this time has an optional branch incase we want to override the defaults in the request. So in our handle method, we can simple check if the input has the argument ‘branch’ and if so, we can parse this and set the data for the request. We then give a little feedback to the CLI, letting the user know what we are doing – and send the request. If all goes well at this point we can simply output a message informing the user that GitHub accepted the request. However if something goes wrong, we want to throw the specific exception, at least during develoment.

The main caveat with this last request is that our workflow is set up to be triggered by a webhook by adding a new on item into the workflow:

1on: workflow_dispatch

That is it! We are using Saloon and Laravel to not only list repository workflows, but if configured correctly we can also trigger them to be ran on demand :muscle:

As I said at the beginning of this tutorial, there are many ways to approach API integrations, but one thing is for certain – using Saloon makes it clean and easy, but also quite delightful to use.

Laravel News

Matt Layman: You Don’t Need JavaScript

https://www.mattlayman.com/img/2022/bbkPxxxCV6M.jpgWhat If I Told You… You Don’t Need JavaScript.
This talk explores why JavaScript is not good fit for most web apps.
I then show how most web apps can do dynamic things using htmx. htmx is an extension library to make HTML markup better.
I present examples of AJAX fetching and deletion. The presentation includes a dynamic search and how to implement infinite scrolling with a trivial amount of code.Planet Python

‘Stormgate’ is a new free-to-play RTS from the director of ‘Starcraft 2’

http://img.youtube.com/vi/lLMEIMCmS44/0.jpg

In 2020, Starcraft 2 production director Tim Morten left Blizzard to start Frost Giant Studios. At Summer Game Fest, he finally showed off what he and his team have been working on for the past two years. We got our first look at Stormgate, a new free-to-play real-time strategy game that runs on Unreal Engine 5. Morten didn’t share too many details on the project but said the game would feature two races at launch.  

Frost Giant features some serious talent. In addition to Morten, former Warcraft 3: The Frozen Throne campaign designer Tim Campbell is part of the team working on Stormgate. Frost Giant plans to begin beta testing the game next year. 

Engadget

One Reporter’s Road Trip Nightmare Proves the Electric Vehicle Skeptics Right

https://www.louderwithcrowder.com/media-library/image.png?id=29955272&width=980

As much as the environmentalist crowd and proponents of "green" renewable energy enjoy touting the newest technological innovations as something of a godsend, many Americans remain skeptical of the advances. Even the seemingly unstoppable climb in gas prices fails to move many who simply don’t believe their neighbor’s Prius is the solution to their fiscal woes. Perhaps it’s just intransigence. Maybe Americans simply aren’t prepared to adopt the new technology simply because we’re stuck in our ways–who doesn’t enjoy hearing the purr of a finely tuned vehicle or the roar as you stomp the gas at a light that has only just turned green.

But it may also be that people have weighed to pros and cons, looked into the capabilities, and made an educated choice based on all the relevant factors. If they haven’t, or if they are still thinking about making the move to an electric vehicle, the story of one journalist’s nightmare road trip might be the final bit of information they need to make a decision.

Writing for the Wall Street Journal, Rachel Wolfe prepared and planned for a recent trip with all the glee of a child counting down the days to Christmas the year before finding out Santa doesn’t actually exist. She is hopeful to the point of giddiness, unaware that the fantasy she’s been told is all about to come crashing down, in due time.

She’s responsible about the planning, outlining the entire itinerary, and planning every stop to charge her rented Kia EV6. She’s so sure about her plan that she invites along her friend who has a hard time to meet–a shift at work at the end of the trip.

What Wolfe and her friend Mack find out, however, is the truth.

The reality of the electric vehicle infrastructure immediately slaps the duo in the face. Not only are chargers apparently divided into quick chargers and, well, not, but among those chargers there exists an extremely and ultimately disconcerting caveat to the moniker "quick charger." This categorization is given to those machines capable of supplying from 24-350kW, a range that proves troublesome as it translates into far longer charge times when the machine you’ve stopped at is on the lower end of that spectrum and even worse when it can’t even meet the minimum standard, like the machine Wolfe came across in the first leg of her trip.

From there, it only spirals. Suffice it to say, deficiencies in the charging infrastructure as well as flaws in the vehicle itself, which especially suffers through inclement weather, repeatedly deal blow after blow, heartache after headache all the way to the end. What’s more, it would seem the universe was attempting to warn the two women about their decision, as person after person along the way voiced apprehension, skepticism, and regret regarding the purchase of electric vehicles.

At one point, to conserve energy, Wolfe and her friend frantically work to cut power consumption to prevent a breakdown on the road in the middle of a storm. "To save power, we turn off the car’s cooling system and the radio, unplug our phones and lower the windshield wipers to the lowest possible setting while still being able to see. Three miles away from the station, we have one mile of estimated range."

Don’t worry. This isn’t about to turn into a horror movie where they break down in the middle of the night or something. They make it to the next charging station but only right in the nick of time.

"At zero miles, we fly screeching into a gas-station parking lot. A trash can goes flying and lands with a clatter to greet us."

They also manage to make it back to Chicago in just enough time for an emotionally drained and physically exhausted Mack to walk into a shift at work, at least she didn’t miss it.

In the end, even Wolfe was forced to come to terms with the reality of the present state of EVs and their support, obviously coming to the conclusion that they aren’t all they’re cracked up to be.

"The following week, I fill up my Jetta at a local Shell station. Gas is up to $4.08 a gallon. I inhale deeply. Fumes never smelled so sweet."

While I’ve editorialized quite a bit and condensed her story down to just a few snippets, you can rest assured the entire story is there. And for those of us who have honestly kept an eye on the burgeoning electric vehicle industry, absolutely none of this comes as a shock. The tech is getting there, and I will even concede that it may well become something great and reliable in the future, but that future is not yet upon us. So, while politicians and celebrities laugh at Americans still driving around in gasoline-powered vehicles, pointing at the skyrocketing gas prices and laughing at those forced to pay them, the truth is that even those financially capable of making the change to an EV will find themselves wrestling with the same issues encountered by Wolfe during her brief trip.

Now, I just moved. The drive was about 350 miles one way, and I did it on a single tank of gas. And while even my trip suffered from a few spats of rainy weather, I never had to stop or sacrifice my AC or unplug my phone, turn off the radio, or worry about if my windshield wipers were going to suck up the last bit of fuel in the tank. And if I had run low on fuel, I knew that any gas station could fill me up. And the high prices notwithstanding, that’s a level of peace of mind no EV driver can say to have. Or rather, not if they want to pull out of the station in under an hour.

Perhaps what is necessary is not to force Americans to make the transition to EVs; this would only serve to stress an already weak infrastructure. What we need is more responsible policies to lower gasoline prices, make driving more affordable, and provide the requisite amount of time to build that infrastructure if and or when that transition occurs naturally.

The Louder with Crowder Dot Com Website is on Instagram now! Follow us at @lwcnewswire and tell a friend!


Kamala DODGES Question On The Idiotic ‘Green New Deal’ | Louder With Crowder

www.youtube.com

Louder With Crowder

You Thought ‘Bugdom’ and ‘Nanosaur’ Were Lost Forever

https://i.kinja-img.com/gawker-media/image/upload/c_fill,f_auto,fl_progressive,g_center,h_675,pg_1,q_80,w_1200/3f2bea8263f27df10227d92520762547.jpg

These days, the only pre-installed game you’ll find on Mac is an exciting, strategy-based war simulator pitting royal factions against one another. And by that, I mean Chess. But now you can get unique, fun, classic games like Bugdom, Nanosaur, and Cro-Mag Rally. I thought these titles were lost for good, but as it turns out, you can still play them.

I grew up with the iMac G3. To the outside world it certainly wasn’t a gaming machine, but to me it was a premiere PC. I was able to play the games I wanted to play, which were usually the Mac’s two Harry Potter ports (those soundtracks, though), but my favorite part of the G3 was the pre-installed titles: I didn’t have an N64, PlayStation, or GameCube, but I had Bugdom, Nanosaur, and Cro-Mag Rally. And that was alright with me.

What happened to Bugdom, Nanosaur, and Cro-Mag Rally?

In case you don’t have the fond memories of these titles, here’s a quick summary: Bugdom has you playing as a pill bug traversing 10 levels to save your world from an invasion of enemy ants. (It’s great, I promise.) In Nanosaur, you’re a dinosaur armed to the teeth, outrunning other dinosaurs in an effort to steal their eggs. (Again, it’s great.) And Cro-Mag Rally is a kart racer game that’s set in the ancient world, complete with “time-appropriate” karts and weapons.

These games wouldn’t be a sell in 2022, but they did push some boundaries for Mac gaming and 3D development back in the day.

It doesn’t end there, though: The iMac G5 also shipped with two unique titles: Nanosaur 2, a sequel to the original dino shooter (this time starring a murderous pterodactyl), and Marble Blast Gold, in which you controlled a marble through a series of progressively challenging race tracks to the finish line. To give credit where credit’s due, Pangea Software developed most of these games, plus plenty of other games you could purchase separately, while Marble Blast Gold was developed by GarageGames.

The problem with these old games is they were written for Mac hardware (PowerPC) that is no longer supported. The original game files exist, but if you download them to your Mac today, you won’t be able to open them. With the exception of mobile ports (which I’ll cover below), I thought most of these games were essentially lost forever. Luckily, there’s a way to replay them on your current hardware, through both mobile ports, as well as total rewrites of the games’ original code.

How to download classic Mac games, or play online

The two titles available as Mac downloads right now are Bugdom and Nanosaur. These games have been rewritten by developer Jorio for macOS, Windows, and Linux, which allows you to play the original games as they were on your current machine. To install these games on your computer, follow the links above, then choose your particular OS from the list of download links. It’s a nostalgia trip, for sure. Do I miss playing these things on that classic CRT display with the matching keyboard and hockey puck mouse? Sure. But after years of not being able to play Bugdom outside of my own memories, I’ll take it.

The easiest one to play, though, is Marble Blast Gold. The game and its sequel were ported by developer Vanilagy as web apps, meaning you can play right in your browser. Head to this site, then click the marble in the top left to choose Marble Blast Gold. You’ll find all levels already unlocked, plus over 2,000 custom levels designed by other players.

Cro-Mag Rally and Nanosaur 2 haven’t been rewritten for modern Macs, unfortunately, but you can play the games’ ports on iOS and iPadOS as a $1.99 download (there’s also a free version of Nanosaur 2 with ads). Nanosaur 2 is mostly how I remember it, but I’m a bit disappointed with the Cro-Mag Rally situation. Don’t get me wrong—I’m thrilled this game is ready to play in 2022 in any form, but this version isn’t the one I really want. Cro-Mag Rally on Mac OS X came with additional game modes, plus a settings pane that let you adjust the physics of the game. I’d love to experience those parts of Cro-Mag Rally again, but it doesn’t look like that’s happening anytime soon.

 

Lifehacker

Akaunting 3.0


We are a podcast-first media company based in Estonia and Turkey. We are required to follow two different tax regimes in two countries. Akaunting helps us focus on what matters to grow our business
instead of being tangled in invoicing details. And their App Store is beneficial if you are trying to customize the bookkeeping process.

Laravel News Links

In Honor Of Pride Month Chick-Fil-A Waffle Fries Will Be Seasoned With Salt From Lot’s Wife

https://media.babylonbee.com/articles/article-11445-1.jpg

U.S.—Chick-Fil-A has finally come around to celebrating pride month this year. The fast-food company has announced that throughout June, all waffle fries will be covered in salt from Lot’s wife.

“Other companies go in for rainbow flags and squeezing in PRIDE everywhere they can, but we wanted our celebration of pride month to be a bit more…biblical,” said Dan Cathy at a recent press release. “Now with every delicious, perfectly seasoned bite of waffle fry customers will be reminded how God celebrates Pride.”

“Wow! That IS salty. Man, I needed this reminder to flee from sexual sin,” said Chick-fil-a patron Brenda Lovelace. “I don’t want to end up as a pillar of salt just like Lot’s wife!”

Much to Chick-fil-a’s surprise, this move to faithfully honor pride month has been met with intense backlash from the LGBT community. “WHAT?! You’re supposed to celebrate by changing all your bags to rainbow flags and putting Drag Queens in the playplace ball pit!” said queer-activist Brandley Xenus. “This doesn’t count!” 

At publishing time, Dan Cathy also announced that if you tell any employee the secret phrase “I take kids to Drag shows,” they will celebrate by placing a millstone to your neck and tossing you into the ocean.


Are you a woman? It’s hard to tell these days. Watch our well-researched video to find out whether you are indeed a woman.

Subscribe to The Babylon Bee on YouTube

The Babylon Bee

Inside an Abandoned Mega Resort

https://theawesomer.com/photos/2022/06/abandoned_orlando_resort_t.jpg

Inside an Abandoned Mega Resort

Link

Built in the early 1970s, the 900+ room Carolando Motor Inn was, for a time, the largest hotel near Disney World. It was part of a larger development that never came to fruition, and after a successful run as a Hyatt, and a failed reopening, it saw its final guests in 2012. Bright Sun Films takes us on a tour of its water-logged ruins.

The Awesomer