Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add RequestTracker and TrackingGuzzleClientFactory #4

Merged
merged 1 commit into from
Feb 14, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ tests export-ignore
.php-cs-fixer.php export-ignore
phpstan.neon export-ignore
phpunit.xml export-ignore
workbench export-ignore
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [2.1.0] - 2024-02-14
### Added
* New classes `RequestTracker` and `TrackingGuzzleClientFactory`. When steps need to execute HTTP requests without the `HttpLoader` from the crawler package (for example when using some REST API SDK), developers are encouraged to utilize either a Guzzle Client instance generated by the `TrackingGuzzleClientFactory` or invoke the `trackHttpResponse()` or `trackHeadlessBrowserResponse()` methods of the `RequestTracker` manually after each request. This enables seamless tracking of requests within the crwl.io app.

## [2.0.0] - 2024-02-07
### Changed
* Require `illuminate/support`, register `ExtensionPackageManager` as a singleton via a new `ServiceProvider` and remove `ExtensionPackageManager::singleton()` and `ExtensionPackageManager::new()` methods.
Expand Down
56 changes: 56 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -201,3 +201,59 @@ To complete the setup, add the `ServiceProvider` to the `extra` section in the `
```

With these configurations in place, your extension package is ready for use. If your extension is private, ensure you grant access to the [crwlrsoft GitHub organization](https://github.com/crwlrsoft). As a super-user on your crwl.io instance, you can then install your extension via the extensions page in the app.

## Custom Steps Performing HTTP Requests Without the crwlr/crawler HttpLoader

In scenarios where your custom steps need to execute HTTP requests that cannot leverage the `HttpLoader` from the `crwlr/crawler` package—such as when utilizing a REST API SDK to retrieve data from an API—you'll need to ensure that every HTTP request is tracked when executing your custom steps within the crwl.io app.

To accomplish this, you have two options:
* Use a Guzzle Client instance generated by the `TrackingGuzzleClientFactory`.
* Alternatively, manually invoke the `trackHttpResponse()` or `trackHeadlessBrowserResponse()` methods of the `RequestTracker` following each request.

### Using a guzzle Client instance

If you want to use a Guzzle `Client` instance for the requests (it's common practice for PHP API SDKs to let you provide your own Guzzle instance), use the `TrackingGuzzleClientFactory` from this package:

```php
use Crwlr\CrwlExtensionUtils\TrackingGuzzleClientFactory;

// Let the factory be resolved by the laravel service container.
$factory = app()->make(TrackingGuzzleClientFactory::class);

$client = $factory->getClient();
```

You can also pass your custom Guzzle configuration as an argument:

```php
$client = $factory->getClient(['allow_redirects' => false]);
```

### Using the RequestTracker

In scenarios where utilizing a Guzzle `Client` instance for requests is not feasible, you need to call either `RequestTracker::trackHttpResponse()` or if your request was executed using a headless browser `RequestTracker::trackHeadlessBrowserResponse()`.

```php
use Crwlr\CrwlExtensionUtils\RequestTracker;

// Let the tracker be resolved by the laravel service container.
$tracker = app()->make(RequestTracker::class);

// Execute your request however you want...

$tracker->trackHttpResponse();

// or

$tracker->trackHeadlessBrowserResponse();
```

If you can provide request/response instances implementing the PSR-7 `RequestInterface` and/or `ResponseInterface`, please do so:

```php
$tracker->trackHttpResponse($request, $response);

// or

$tracker->trackHeadlessBrowserResponse($request, $response);
```
28 changes: 24 additions & 4 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
"Tests\\": "tests/",
"Workbench\\App\\": "workbench/app/",
"Workbench\\Database\\Factories\\": "workbench/database/factories/",
"Workbench\\Database\\Seeders\\": "workbench/database/seeders/"
}
},
"authors": [
Expand All @@ -27,14 +30,31 @@
"require-dev": {
"pestphp/pest": "^2.4",
"friendsofphp/php-cs-fixer": "^3.48",
"phpstan/phpstan": "^1.10"
"phpstan/phpstan": "^1.10",
"pestphp/pest-plugin-laravel": "^2.2",
"orchestra/testbench": "^8.21"
},
"scripts": {
"test": "@php vendor/bin/pest",
"cs": "@php vendor/bin/php-cs-fixer fix -v --dry-run",
"cs-fix": "@php vendor/bin/php-cs-fixer fix -v",
"stan": "@php vendor/bin/phpstan analyse -c phpstan.neon",
"add-git-hooks": "@php bin/add-git-hooks"
"add-git-hooks": "@php bin/add-git-hooks",
"post-autoload-dump": [
"@clear",
"@prepare"
],
"clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
"prepare": "@php vendor/bin/testbench package:discover --ansi",
"build": "@php vendor/bin/testbench workbench:build --ansi",
"serve": [
"Composer\\Config::disableProcessTimeout",
"@build",
"@php vendor/bin/testbench serve"
],
"lint": [
"@php vendor/bin/phpstan analyse"
]
},
"extra": {
"laravel": {
Expand All @@ -48,4 +68,4 @@
"pestphp/pest-plugin": true
}
}
}
}
50 changes: 50 additions & 0 deletions src/RequestTracker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

namespace Crwlr\CrwlExtensionUtils;

use Closure;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;

final class RequestTracker
{
/**
* @var Closure[]
*/
private array $onHttpResponse = [];

/**
* @var Closure[]
*/
private array $onHeadlessBrowserResponse = [];

public function onHttpResponse(Closure $closure): self
{
$this->onHttpResponse[] = $closure;

return $this;
}

public function onHeadlessBrowserResponse(Closure $closure): self
{
$this->onHeadlessBrowserResponse[] = $closure;

return $this;
}

public function trackHttpResponse(?RequestInterface $request = null, ?ResponseInterface $response = null): void
{
foreach ($this->onHttpResponse as $closure) {
$closure->call($this, $request, $response);
}
}

public function trackHeadlessBrowserResponse(
?RequestInterface $request = null,
?ResponseInterface $response = null
): void {
foreach ($this->onHeadlessBrowserResponse as $closure) {
$closure->call($this, $request, $response);
}
}
}
8 changes: 5 additions & 3 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

namespace Crwlr\CrwlExtensionUtils;

use Illuminate\Contracts\Foundation\Application;

class ServiceProvider extends \Illuminate\Support\ServiceProvider
{
public function register(): void
{
$this->app->singleton(ExtensionPackageManager::class, function (Application $app) {
$this->app->singleton(ExtensionPackageManager::class, function () {
return new ExtensionPackageManager();
});

$this->app->singleton(RequestTracker::class, function () {
return new RequestTracker();
});
}
}
31 changes: 31 additions & 0 deletions src/TrackingGuzzleClientFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Crwlr\CrwlExtensionUtils;

use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Middleware;
use Psr\Http\Message\ResponseInterface;

final class TrackingGuzzleClientFactory
{
public function __construct(private readonly RequestTracker $requestTracker) {}

/**
* @param mixed[] $withOptions
*/
public function getClient(array $withOptions = []): Client
{
$stack = array_key_exists('handler', $withOptions) ? $withOptions['handler'] : HandlerStack::create();

$stack->push(Middleware::mapResponse(function (ResponseInterface $response) {
$this->requestTracker->trackHttpResponse(response: $response);

return $response;
}));

$withOptions['handler'] = $stack;

return new Client($withOptions);
}
}
21 changes: 21 additions & 0 deletions testbench.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
providers:
# - Workbench\App\Providers\WorkbenchServiceProvider

migrations:
- workbench/database/migrations

seeders:
- Workbench\Database\Seeders\DatabaseSeeder

workbench:
start: '/'
install: true
discovers:
web: true
api: false
commands: false
components: false
views: false
build: []
assets: []
sync: []
76 changes: 45 additions & 31 deletions tests/Pest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,42 +2,40 @@

use Crwlr\Crawler\Steps\Step;
use Crwlr\Crawler\Steps\StepInterface;
use Crwlr\CrwlExtensionUtils\RequestTracker;
use Crwlr\CrwlExtensionUtils\StepBuilder;
use Crwlr\CrwlExtensionUtils\TrackingGuzzleClientFactory;
use GuzzleHttp\Client;
use Illuminate\Contracts\Container\BindingResolutionException;
use Symfony\Component\Process\Process;
use Tests\TestCase;

/*
|--------------------------------------------------------------------------
| Test Case
|--------------------------------------------------------------------------
|
| The closure you provide to your test functions is always bound to a specific PHPUnit test
| case class. By default, that class is "PHPUnit\Framework\TestCase". Of course, you may
| need to change it using the "uses()" function to bind a different classes or traits.
|
*/
uses(TestCase::class)->in(__DIR__);

// uses(Tests\TestCase::class)->in('Feature');
class TestServerProcess
{
public static ?Process $process = null;
}

uses()
->group('integration')
->beforeEach(function () {
if (!isset(TestServerProcess::$process)) {
TestServerProcess::$process = Process::fromShellCommandline(
'php -S localhost:8000 ' . __DIR__ . '/_Integration/Server.php'
);

/*
|--------------------------------------------------------------------------
| Expectations
|--------------------------------------------------------------------------
|
| When you're writing tests, you often need to check that values meet certain conditions. The
| "expect()" function gives you access to a set of "expectations" methods that you can use
| to assert different things. Of course, you may extend the Expectation API at any time.
|
*/
TestServerProcess::$process->start();

/*
|--------------------------------------------------------------------------
| Functions
|--------------------------------------------------------------------------
|
| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
| project that you don't want to repeat in every file. Here you can also expose helpers as
| global functions to help you to reduce the number of lines of code in your test files.
|
*/
usleep(100000);
}
})
->afterAll(function () {
TestServerProcess::$process?->stop(3, SIGINT);

TestServerProcess::$process = null;
})
->in('_Integration');

function helper_makeStepBuilder(string $stepId): StepBuilder
{
Expand Down Expand Up @@ -68,3 +66,19 @@ protected function invoke(mixed $input): Generator
}
};
}

/**
* @throws BindingResolutionException
*/
function helper_getTrackingGuzzleClient(): Client
{
return app()->make(TrackingGuzzleClientFactory::class)->getClient();
}

/**
* @throws BindingResolutionException
*/
function helper_getRequestTracker(): RequestTracker
{
return app()->make(RequestTracker::class);
}
Loading