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

Improve phpdoc typing #4

Merged
merged 2 commits into from
Jun 7, 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
576 changes: 306 additions & 270 deletions composer.lock

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions phpcs.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<?xml version="1.0"?>
<ruleset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" name="PhpCs_AnyFinder" xsi:noNamespaceSchemaLocation="./vendor/squizlabs/php_codesniffer/phpcs.xsd">
<rule ref="./vendor/jerodev/code-styles/phpcs.xml" />
<rule ref="./vendor/jerodev/code-styles/phpcs.xml">
<!-- This does not play well with tests -->
<exclude name="PSR1.Methods.CamelCapsMethodName.NotCamelCaps" />
</rule>

<file>src</file>
<file>tests</file>
</ruleset>
13 changes: 7 additions & 6 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ includes:

parameters:

level: 7
level: 7

paths:
- src
- tests
paths:
- src
- tests

checkGenericClassInNonGenericObjectType: false
checkMissingIterableValueType: false
ignoreErrors:
- identifier: missingType.generics
- identifier: missingType.iterableValue
12 changes: 10 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,14 @@ If a selector is given, the current nodes will first be filtered.
$nodes->exists('a.active');
```

### `filter(closure $closure)`
Filters the current node collection based on a given closure.
```php
$nodes->filter(static function (NodeFilter $node) {
return $node->text() === 'foo';
});
```

### `first(?string $selector = null)`
Returns the first element of the node collection.
If a selector is given, the current nodes will first be filtered.
Expand Down Expand Up @@ -115,10 +123,10 @@ $nodes->texts('nav > a');
```

### `whereHas(closure $closure)`
Filters the current node collection based on a given closure.
Filter nodes that contain child nodes that fulfill the filter described by the closure
```php
$nodes->whereHas(static function (NodeFilter $node) {
return $node->text() === 'foo';
return $node->first('a[href]');
});
```

Expand Down
4 changes: 2 additions & 2 deletions src/NodeFilter/NodeCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,7 @@ public function text(?string $selector = null): ?string

public function texts(): array
{
return $this->each(static fn (NodeCollection $n) => $n->text());
return $this->each(static fn (NodeFilter $n) => $n->text());
}

public function whereHas(Closure $closure): NodeFilter
Expand Down Expand Up @@ -192,7 +192,7 @@ public function whereHasText(?string $value = null, bool $trim = true, bool $exa
return $text === $value;
}

return \strpos($text, $value) !== false;
return \str_contains($text, $value);
});
}

Expand Down
16 changes: 8 additions & 8 deletions src/NodeFilter/NodeFilter.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,11 @@ public function count(): int;
* Filter the current nodes and pass them to a defined closure.
* The closure will be passed each DomNode as a SingleNode object and can return any value from it.
*
* @param Closure|string|null $selector
* @param Closure|null $closure
* @template T
* @param Closure(NodeFilter, int):T|string|null $selector
* @param Closure(NodeFilter, int):T|null $closure
* @param int|null $max Optional, The maximum number of nodes to loop over.
* @return array
* @return ($closure is Closure ? array<T> : ($selector is Closure ? array<T> : array<NodeFilter>))
*/
public function each($selector = null, ?Closure $closure = null, ?int $max = null): array;

Expand All @@ -44,7 +45,7 @@ public function exists(?string $selector = null): bool;
* Filter a list of nodes using a closure that returns a boolean.
* The closure gets passed a node filter to test upon.
*
* @param Closure $closure
* @param Closure(NodeFilter):bool $closure
* @return NodeFilter
*/
public function filter(Closure $closure): NodeFilter;
Expand Down Expand Up @@ -112,15 +113,14 @@ public function text(?string $selector = null): ?string;
/**
* Returns the text content of all nodes in the current collection.
*
* @return string[]
* @return array<string>
*/
public function texts(): array;

/**
* Filter the current nodes by their child nodes.
* The closure gets a NodeFilter instance that can be used to define how the nodes should be filtered.
* Filter nodes that contain child nodes that fulfill the filter described by the closure
*
* @param Closure $closure
* @param Closure(NodeFilter):NodeFilter $closure
* @return NodeFilter
*/
public function whereHas(Closure $closure): NodeFilter;
Expand Down
12 changes: 7 additions & 5 deletions src/NodeFilter/Traits/EnumeratesValues.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@
trait EnumeratesValues
{
/**
* @template T
* @param DOMNodeList $nodes
* @param Closure|string|null $selector
* @param Closure|null $closure
* @param Closure(NodeFilter, int):T|string|null $selector
* @param Closure(NodeFilter, int):T|null $closure
* @param int|null $max
* @return array
* @return ($closure is Closure ? array<T> : ($selector is Closure ? array<T> : array<NodeFilter>))
*/
protected function internalEach(DOMNodeList $nodes, $selector = null, ?Closure $closure = null, ?int $max = null): array
{
Expand All @@ -38,11 +39,12 @@ protected function internalEach(DOMNodeList $nodes, $selector = null, ?Closure $
}

$values = [];
$index = 0;
foreach ($nodes as $node) {
\assert($node instanceof DOMNode);

if ($closure instanceof Closure) {
$values[] = $closure(new NodeCollection($node));
$values[] = $closure(new NodeCollection($node), $index++);
} else {
$values[] = new NodeCollection($node);
}
Expand All @@ -59,7 +61,7 @@ protected function internalEach(DOMNodeList $nodes, $selector = null, ?Closure $
* filter nodes directly on DOMNode level
*
* @param DOMNodeList $nodes
* @param Closure $callback
* @param Closure(DOMNode):bool $callback
* @return NodeFilter
*/
protected function internalFilter(DOMNodeList $nodes, Closure $callback): NodeFilter
Expand Down
34 changes: 23 additions & 11 deletions tests/NodeFilter/NodeCollectionTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ final class NodeCollectionTest extends TestCase
protected function setUp(): void
{
$this->node = new NodeCollection(
$this->createDOMNodes()
$this->createDOMNodes(),
);
}

Expand Down Expand Up @@ -110,7 +110,7 @@ public function it_should_get_first_element(): void
{
$this->assertEquals(
'one',
$this->node->querySelector('li')->first()->text()
$this->node->querySelector('li')->first()->text(),
);
}

Expand All @@ -119,7 +119,7 @@ public function it_should_get_last_element(): void
{
$this->assertEquals(
'four',
$this->node->querySelector('li')->last()->text()
$this->node->querySelector('li')->last()->text(),
);
}

Expand All @@ -128,7 +128,7 @@ public function it_should_get_nth_element(): void
{
$this->assertEquals(
'two',
$this->node->querySelector('li')->nth(1)->text()
$this->node->querySelector('li')->nth(1)->text(),
);
}

Expand All @@ -137,12 +137,12 @@ public function it_should_get_text_from_element(): void
{
$this->assertEquals(
'one',
$this->node->querySelector('li')->text()
$this->node->querySelector('li')->text(),
);

$this->assertEquals(
'three',
$this->node->querySelector('li.third')->text()
$this->node->querySelector('li.third')->text(),
);
}

Expand All @@ -151,12 +151,12 @@ public function it_should_get_text_from_elements(): void
{
$this->assertEquals(
['one', 'two', 'three', 'four'],
$this->node->querySelector('li')->texts()
$this->node->querySelector('li')->texts(),
);

$this->assertEquals(
['three'],
$this->node->querySelector('li.third')->texts()
$this->node->querySelector('li.third')->texts(),
);
}

Expand All @@ -165,13 +165,25 @@ public function it_should_iterate_nodes(): void
{
$this->assertEquals(
['one', 'two', 'four'],
$this->node->each('li:not([class])', static fn (NodeFilter $n) => $n->text())
$this->node->each('li:not([class])', static fn (NodeFilter $n) => $n->text()),
);

$this->assertEquals(
['one', 'two', 'three', 'four'],
$this->node->querySelector('li')->each(static fn (NodeFilter $n) => $n->text())
$this->node->querySelector('li')->each(static fn (NodeFilter $n) => $n->text()),
);

$this->assertEquals(
[1, 2, 3, 4],
$this->node->querySelector('li')->each(static fn (NodeFilter $_, int $i) => $i+1),
);

// No return type should not throw PHPStan error
$result = [];
$this->node->each('li', static function () use (&$result) {
$result[] = 0;
});
$this->assertEquals([0, 0, 0, 0], $result);
}

/** @test */
Expand All @@ -194,7 +206,7 @@ private function createDOMNodes(): DOMNodeList
{
$html = <<<'HTML'
<!DOCTYPE html>
<html>
<html lang="en">
<body>
<div>
<p>Intro</p>
Expand Down
19 changes: 9 additions & 10 deletions tests/NodeFilter/NullNodeTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

namespace Jerodev\Diggy\Tests\NodeFilter;

use Jerodev\Diggy\NodeFilter\NodeCollection;
use Jerodev\Diggy\NodeFilter\NodeFilter;
use Jerodev\Diggy\NodeFilter\NullNode;
use PHPUnit\Framework\TestCase;
Expand All @@ -21,7 +20,7 @@ public function it_should_return_empty_array_on_each(): void
{
$this->assertEquals(
[],
$this->node->each('.foo', static fn (NodeCollection $n) => $n)
$this->node->each('.foo', static fn (NodeFilter $n) => $n),
);
}

Expand All @@ -30,7 +29,7 @@ public function it_should_return_empty_array_on_texts(): void
{
$this->assertEquals(
[],
$this->node->texts()
$this->node->texts(),
);
}

Expand All @@ -51,7 +50,7 @@ public function it_should_return_null_node_on_has_attribute(): void
{
$this->assertInstanceOf(
NullNode::class,
$this->node->whereHasAttribute('class')
$this->node->whereHasAttribute('class'),
);
}

Expand All @@ -60,7 +59,7 @@ public function it_should_return_null_node_on_has_text(): void
{
$this->assertInstanceOf(
NullNode::class,
$this->node->whereHasText('bar')
$this->node->whereHasText('bar'),
);
}

Expand All @@ -69,7 +68,7 @@ public function it_should_return_null_node_on_query_selector(): void
{
$this->assertInstanceOf(
NullNode::class,
$this->node->querySelector('.foo')
$this->node->querySelector('.foo'),
);
}

Expand All @@ -78,7 +77,7 @@ public function it_should_return_null_node_on_where_has(): void
{
$this->assertInstanceOf(
NullNode::class,
$this->node->whereHas(static fn (NodeFilter $f) => $f->querySelector('.foo'))
$this->node->whereHas(static fn (NodeFilter $f) => $f->querySelector('.foo')),
);
}

Expand All @@ -87,23 +86,23 @@ public function it_should_return_null_node_on_xpath(): void
{
$this->assertInstanceOf(
NullNode::class,
$this->node->xPath('.foo')
$this->node->xPath('.foo'),
);
}

/** @test */
public function it_should_return_null_on_get_attribute(): void
{
$this->assertNull(
$this->node->attribute('class')
$this->node->attribute('class'),
);
}

/** @test */
public function it_should_return_null_on_text(): void
{
$this->assertNull(
$this->node->text()
$this->node->text(),
);
}
}
Loading