Skip to content
Draft
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
24 changes: 9 additions & 15 deletions .github/LOCAL_TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,20 @@ This project is configured to run GitHub Actions locally using [nektos/act](http

## Quick Start

### Fastest Path: Validate with PHP 8.0 Only
### Fastest Path: Validate with PHP 8.5

For developers making changes, use this to validate quickly before pushing:

```bash
# Run full CI validation with PHP 8.0 (lowest supported version)
# This is the recommended default - CI will test all versions
# Run full CI validation with PHP 8.5 (the only supported version)
./bin/check
```

### Full act Usage

```bash
# Run all workflows (push event) with PHP 8.0
./bin/act --matrix php-versions:8.0
# Run all workflows (push event) with PHP 8.5
./bin/act --matrix php-versions:8.5

# Run workflows for pull request event
./bin/act pull_request
Expand All @@ -35,7 +34,7 @@ For developers making changes, use this to validate quickly before pushing:
./bin/act -l

# Run with specific PHP version matrix
./bin/act --matrix php-versions:8.3
./bin/act --matrix php-versions:8.5

# Dry run (don't execute, just show what would run)
./bin/act -n
Expand Down Expand Up @@ -95,21 +94,16 @@ Run with verbose output:

## Matrix Testing

**Recommended for CI validation**: Always test with PHP 8.0 only (lowest supported version). CI will automatically run the full matrix across all supported versions:
**Recommended for CI validation**: PHP-Spider requires PHP >= 8.5, so there is a single supported version to test:

```bash
# Default and recommended - validates with PHP 8.0
# Default and recommended - validates with PHP 8.5
./bin/check

# Explicitly with PHP 8.0 (same as check)
./bin/act --matrix php-versions:8.0

# To test a different version manually:
./bin/act --matrix php-versions:8.3
# Explicitly with PHP 8.5 (same as check)
./bin/act --matrix php-versions:8.5
```

**Note**: Do not run the full matrix locally (`--matrix php-versions:8.0,8.1,8.2,8.3`). CI handles cross-version testing automatically.

## What Gets Tested Locally

The local execution runs:
Expand Down
12 changes: 6 additions & 6 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# PHP-Spider – Copilot Guide

- Project shape: a configurable crawler around Guzzle + Symfony components (dom-crawler, css-selector, finder, event-dispatcher) and vdb/uri. Entry point and orchestration live in [src/Spider.php](src/Spider.php); examples are in [example/](example/).
- Project shape: a configurable crawler around Guzzle + Symfony components (dom-crawler, css-selector, finder, event-dispatcher) and the native `Uri\Rfc3986\Uri` class (`ext-uri`). Entry point and orchestration live in [src/Spider.php](src/Spider.php); examples are in [example/](example/).
- Crawl loop: `Spider::crawl()` seeds the queue, sets the persistence handler spider id, fires a `spider.crawl.pre_crawl` event, then iterates `doCrawl()` pulling URIs from the queue, downloading, persisting, dispatching `spider.crawl.resource.persisted`, and feeding discoveries back into the queue.
- Traversal and queueing: [src/QueueManager/InMemoryQueueManager.php](src/QueueManager/InMemoryQueueManager.php) defaults to depth-first; switch with `setTraversalAlgorithm(ALGORITHM_BREADTH_FIRST)`. `maxQueueSize` stops discovery once reached (throws `MaxQueueSizeExceededException`); enqueue emits `spider.crawl.post.enqueue`.
- Discovery pipeline: [src/Discoverer/DiscovererSet.php](src/Discoverer/DiscovererSet.php) holds discoverers + prefetch filters, tracks already-seen URIs, and enforces `maxDepth` (default 3) to stop recursion. Register discoverers via `addDiscoverer()` and filters via `addFilter()`.
- Download pipeline: [src/Downloader/Downloader.php](src/Downloader/Downloader.php) uses a `RequestHandlerInterface` (default [GuzzleRequestHandler](src/RequestHandler/GuzzleRequestHandler.php)) and a `PersistenceHandlerInterface` (default [MemoryPersistenceHandler](src/PersistenceHandler/MemoryPersistenceHandler.php)). `downloadLimit` caps persisted resources. Postfetch filters run before persistence and emit `spider.crawl.filter.postfetch`.
- Resource model: [src/Resource.php](src/Resource.php) wraps `DiscoveredUri` + PSR-7 response and lazily creates a Symfony `Crawler` with response body and content-type; it serializes by storing the raw message for file-based persistence.
- Persistence options: in-memory for small runs; file-based handlers in [src/PersistenceHandler](src/PersistenceHandler) write per-spider-id directories and serialize resources (`FileSerializedResourcePersistenceHandler` keeps the PSR-7 response intact). Set `setSpiderId()` before persisting.
- URI model: [src/Uri/DiscoveredUri.php](src/Uri/DiscoveredUri.php) decorates `vdb/uri` with `depthFound` to drive depth filtering and normalization/de-duplication.
- URI model: [src/Uri/DiscoveredUri.php](src/Uri/DiscoveredUri.php) decorates the native `Uri\Rfc3986\Uri` with `depthFound` to drive depth filtering and de-duplication. `Uri\Rfc3986\Uri` normalizes case, dot-segments, and percent-encoding while parsing, so no separate normalization step is needed.
- Filters: prefetch filters live in [src/Filter/Prefetch](src/Filter/Prefetch) (e.g., `RestrictToBaseUriFilter`, `AllowedHostsFilter`, regex-based `UriFilter`, robots.txt-aware `RobotsTxtDisallowFilter`); postfetch filters in [src/Filter/Postfetch](src/Filter/Postfetch) (e.g., `MimeTypeFilter`). Filters return true to skip.
- Events and extensibility: events declared in [src/Event/SpiderEvents.php](src/Event/SpiderEvents.php); dispatcher shared via [DispatcherTrait](src/Event/DispatcherTrait.php). Typical listeners: pre-request throttling ([src/EventListener/PolitenessPolicyListener.php](src/EventListener/PolitenessPolicyListener.php) hooks `spider.crawl.pre_request`) and stats collection example in [example/lib/Example/StatsHandler.php](example/lib/Example/StatsHandler.php).
- HTTP handling: default Guzzle handler throws on 4XX/5XX; to keep crawling on errors, supply a custom `RequestHandlerInterface` (see link-checker example referenced in [README](README.md)). Signals (SIGTERM/SIGINT/etc.) trigger `spider.crawl.user.stopped` when running in CLI.
- Key tuning knobs: `DiscovererSet::$maxDepth`, `QueueManager::$maxQueueSize`, `Downloader::setDownloadLimit()`, traversal algorithm, request delay via politeness listener, robots.txt user-agent.
- Coding standards: PSR-0/1/2; codebase targets PHP >= 8.0. Autoload via PSR-4 `VDB\Spider\` from `src/`.
- Coding standards: PSR-0/1/2; codebase targets PHP >= 8.5 (requires `ext-uri`). Autoload via PSR-4 `VDB\Spider\` from `src/`.

## Development & Testing Workflow

Expand All @@ -26,7 +26,7 @@
- **ALWAYS run `./bin/check` before EVERY commit and before creating/updating ANY pull request**
- This is the **single source of truth** for validation
- Runs the complete CI workflow: lint, phpcs (PSR2), phpmd, phan, and phpunit with 100% coverage
- Uses `./bin/act --matrix php-versions:8.0` to run GitHub Actions locally with the lowest supported PHP version
- Uses `./bin/act --matrix php-versions:8.5` to run GitHub Actions locally with the only supported PHP version
- **DO NOT** commit without running `./bin/check` first
- **DO NOT** run individual static analysis tools (phpcs, phpmd, phan) manually - `./bin/check` runs them all correctly

Expand All @@ -41,7 +41,7 @@ php -l src/SomeFile.php # Syntax check (optional, no deps)
./bin/check # Full CI validation (required before commit/PR)

# Equivalents (same as ./bin/check)
./bin/act --matrix php-versions:8.0 # Explicit form of ./bin/check
./bin/act --matrix php-versions:8.5 # Explicit form of ./bin/check
```

## Testing & Static Analysis
Expand Down Expand Up @@ -70,7 +70,7 @@ php -l src/SomeFile.php # Syntax check (optional, no deps)
- Linting (PSR-1/2 compliance)
- phpcs, phpmd, phan static analysis
- phpunit with 100% code coverage
- All tests on PHP 8.0
- All tests on PHP 8.5

3. **If any check fails:**
- Fix all issues
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/copilot-setup-steps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ jobs:
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.0'
extensions: dom, pcntl, ast
php-version: '8.5'
extensions: dom, pcntl, ast, uri
tools: composer
coverage: xdebug

Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
fail-fast: false
matrix:
operating-system: [ ubuntu-latest ]
php-versions: [ '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ]
php-versions: [ '8.5' ]

steps:
- uses: actions/checkout@v6
Expand All @@ -23,7 +23,7 @@ jobs:
uses: shivammathur/setup-php@v2 #https://github.com/shivammathur/setup-php
with:
php-version: ${{ matrix.php-versions }}
extensions: ast
extensions: ast, uri
coverage: xdebug

- name: Get Composer Cache Directory
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ There a few requirements for a Pull Request to be accepted:
You can run the full CI pipeline locally using [nektos/act](https://nektosact.com/):

```bash
# Fast path: run the full workflow with PHP 8.0 (recommended)
# Fast path: run the full workflow with PHP 8.5 (recommended)
./bin/check
```

Expand All @@ -167,7 +167,7 @@ Or use the underlying act wrapper directly:
./bin/act

# Run specific PHP version locally
./bin/act --matrix php-versions:8.0
./bin/act --matrix php-versions:8.5

# Run specific job or view available workflows
./bin/act -l
Expand Down
10 changes: 5 additions & 5 deletions bin/check
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
#!/bin/bash
# Quick CI validation script - runs the full workflow with PHP 8.0 only
# Quick CI validation script - runs the full workflow with PHP 8.5 only
# This is the minimal fast path for developers to validate before push
# Usage: ./bin/check [extra options]
# Examples:
# ./bin/check # Run full workflow with PHP 8.0
# ./bin/check # Run full workflow with PHP 8.5
# ./bin/check -v # Run with verbose output
# ./bin/check --pull # Force pull latest image

Expand All @@ -12,6 +12,6 @@ set -e
# Ensure we're in the project root
cd "$(dirname "$0")/.."

# Always run with PHP 8.0 matrix only (no full matrix)
echo "🚀 Running CI validation with PHP 8.0..."
./bin/act -j build --matrix php-versions:8.0 "$@"
# Always run with PHP 8.5 matrix only (no full matrix)
echo "🚀 Running CI validation with PHP 8.5..."
./bin/act -j build --matrix php-versions:8.5 "$@"
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,15 @@
"source": "https://github.com/matthijsvandenbos/php-spider"
},
"require": {
"php": ">=8.0",
"php": ">=8.5",
"ext-dom": "*",
"ext-pcntl": "*",
"ext-uri": "*",
"guzzlehttp/guzzle": "^6.0||^7.0||^8.0",
"symfony/css-selector": "^3.0.0||^4.0.0||^5.0.0||^6.0||^7.0||^8.0",
"symfony/dom-crawler": "^3.0.0||^4.0.0||^5.0.0||^6.0||^7.0||^8.0",
"symfony/finder": "^3.0.0||^4.0.0||^5.0.0||^6.0||^7.0||^8.0",
"symfony/event-dispatcher": "^4.0.0||^5.0.0||^6.0||^7.0||^8.0",
"vdb/uri": "^0.3.2",
"spatie/robots-txt": "^2.0"
},
"require-dev": {
Expand Down
13 changes: 7 additions & 6 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,10 @@ $spider->getDownloader()->addPostFetchFilter(

### DiscoveredUri (src/Uri/DiscoveredUri.php)

Wrapper around `vdb/uri` that adds crawl depth tracking.
Wrapper around the native `Uri\Rfc3986\Uri` class (`ext-uri`, PHP >= 8.5) that adds crawl depth tracking.

**Key properties:**
- `$decorated` - The underlying UriInterface implementation
- `$decorated` - The underlying `Uri\Rfc3986\Uri` instance
- `$depthFound` - Integer depth where this URI was discovered

Depth tracking enables:
Expand All @@ -241,10 +241,11 @@ Depth tracking enables:
- Prioritization strategies

**Normalization:**
URIs are normalized to prevent duplicate crawling of equivalent URLs:
- Trailing slashes standardized
- Default ports removed (80 for http, 443 for https)
- Query parameters sorted (if not filtered out)
`Uri\Rfc3986\Uri` normalizes URIs as part of parsing them (per RFC 3986), so no separate
normalization step is needed to prevent duplicate crawling of equivalent URLs:
- Scheme and host case are folded to lowercase
- Dot segments (`.` and `..`) in the path are resolved
- Percent-encoding is normalized (unreserved characters decoded, hex digits upper-cased)

## Resource Model

Expand Down
29 changes: 10 additions & 19 deletions docs/extending.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ Extract links from JSON API responses:
use VDB\Spider\Discoverer\DiscovererInterface;
use VDB\Spider\Resource;
use VDB\Spider\Uri\DiscoveredUri;
use VDB\Uri\Uri;

class JsonApiDiscoverer implements DiscovererInterface
{
Expand Down Expand Up @@ -67,9 +66,8 @@ class JsonApiDiscoverer implements DiscovererInterface

foreach ($links as $link) {
try {
$uri = new Uri($link);
$discoveredUris[] = new DiscoveredUri($uri, $currentDepth + 1);
} catch (\Exception $e) {
$discoveredUris[] = new DiscoveredUri($link, $currentDepth + 1);
} catch (\Uri\InvalidUriException $e) {
// Skip invalid URIs
continue;
}
Expand Down Expand Up @@ -109,7 +107,6 @@ Parse sitemap XML files:
use VDB\Spider\Discoverer\DiscovererInterface;
use VDB\Spider\Resource;
use VDB\Spider\Uri\DiscoveredUri;
use VDB\Uri\Uri;

class SitemapDiscoverer implements DiscovererInterface
{
Expand All @@ -132,8 +129,7 @@ class SitemapDiscoverer implements DiscovererInterface
$discoveredUris = [];

foreach ($urls as $url) {
$uri = new Uri((string)$url);
$discoveredUris[] = new DiscoveredUri($uri, $currentDepth + 1);
$discoveredUris[] = new DiscoveredUri((string)$url, $currentDepth + 1);
}

return $discoveredUris;
Expand All @@ -156,7 +152,7 @@ Skip URIs with specific file extensions:

```php
use VDB\Spider\Filter\PreFetchFilterInterface;
use VDB\Uri\UriInterface;
use VDB\Spider\Uri\DiscoveredUri;

class FileExtensionFilter implements PreFetchFilterInterface
{
Expand All @@ -171,7 +167,7 @@ class FileExtensionFilter implements PreFetchFilterInterface
);
}

public function match(UriInterface $uri): bool
public function match(DiscoveredUri $uri): bool
{
$path = $uri->getPath();
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
Expand All @@ -196,7 +192,7 @@ Skip URIs matching specific patterns:

```php
use VDB\Spider\Filter\PreFetchFilterInterface;
use VDB\Uri\UriInterface;
use VDB\Spider\Uri\DiscoveredUri;

class UrlPatternFilter implements PreFetchFilterInterface
{
Expand All @@ -207,7 +203,7 @@ class UrlPatternFilter implements PreFetchFilterInterface
$this->excludePatterns = $excludePatterns;
}

public function match(UriInterface $uri): bool
public function match(DiscoveredUri $uri): bool
{
$url = $uri->toString();

Expand Down Expand Up @@ -241,7 +237,7 @@ Different max depths for different domains:

```php
use VDB\Spider\Filter\PreFetchFilterInterface;
use VDB\Uri\UriInterface;
use VDB\Spider\Uri\DiscoveredUri;

class DomainDepthFilter implements PreFetchFilterInterface
{
Expand All @@ -254,17 +250,12 @@ class DomainDepthFilter implements PreFetchFilterInterface
$this->defaultMaxDepth = $defaultMaxDepth;
}

public function match(UriInterface $uri): bool
public function match(DiscoveredUri $uri): bool
{
$host = $uri->getHost();
$maxDepth = $this->domainDepths[$host] ?? $this->defaultMaxDepth;

// DiscoveredUri has depth tracking
if ($uri instanceof \VDB\Spider\Uri\DiscoveredUri) {
return $uri->getDepthFound() > $maxDepth;
}

return false;
return $uri->getDepthFound() > $maxDepth;
}
}
```
Expand Down
1 change: 0 additions & 1 deletion example/lib/Example/LogHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@

use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use VDB\Uri\UriInterface;
use VDB\Spider\Event\SpiderEvents;

class LogHandler implements EventSubscriberInterface
Expand Down
6 changes: 3 additions & 3 deletions example/lib/Example/StatsHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\EventDispatcher\GenericEvent;
use VDB\Spider\Event\SpiderEvents;
use VDB\Uri\UriInterface;
use VDB\Spider\Uri\DiscoveredUri;

class StatsHandler implements EventSubscriberInterface
{
Expand Down Expand Up @@ -116,7 +116,7 @@ public function addToFailed(GenericEvent $event): void
/**
* Get all URIs that were added to the queue
*
* @return UriInterface[]
* @return DiscoveredUri[]
*/
public function getQueued(): array
{
Expand All @@ -126,7 +126,7 @@ public function getQueued(): array
/**
* Get all resources that were successfully persisted
*
* @return UriInterface[]
* @return DiscoveredUri[]
*/
public function getPersisted(): array
{
Expand Down
Loading