Skip to content

Commit b1d640d

Browse files
authored
Add Password Field, drop multiple support in Email Field (#924)
1 parent 1e209cf commit b1d640d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

51 files changed

+642
-571
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ Each persistence implements actions differently. SQL is probably the most full-f
382382

383383
### Introducing Expressions
384384

385-
Smart Fields in Agile Toolkit are represented as objects. Because of inheritance, Fields can be quite diverse at what they do. For example `FieldSqlExpression` and `Field_Expression` can define field through custom SQL or PHP code:
385+
Smart Fields in Agile Toolkit are represented as objects. Because of inheritance, Fields can be quite diverse at what they do. For example `SqlExpressionField` can define field through custom SQL or PHP code:
386386

387387
![GitHub release](docs/images/expression.gif)
388388

composer.json

+1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"johnkary/phpunit-speedtrap": "^3.3",
5656
"phpstan/extension-installer": "^1.1",
5757
"phpstan/phpstan": "^1.0",
58+
"phpstan/phpstan-deprecation-rules": "^1.0",
5859
"phpunit/phpunit": "^9.5.5"
5960
},
6061
"suggest": {

docs/fields.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Conversions between types is what we call :ref:`Typecasting` and there is a
8181
documentation section dedicated to it.
8282

8383
Finally, because Field is a class, it can be further extended. For some
84-
interesting examples, check out :php:class:`Field\\Password`. I'll explain how to
84+
interesting examples, check out :php:class:`PasswordField`. I'll explain how to
8585
create your own field classes and where they can be beneficial.
8686

8787
Valid types are: string, integer, boolean, datetime, date, time.
@@ -167,7 +167,7 @@ Example::
167167
.. php:attr:: read_only
168168
169169
Modifying field that is read-only through set() methods (or array access) will
170-
result in exception. :php:class:`FieldSqlExpression` is read-only by default.
170+
result in exception. :php:class:`SqlExpressionField` is read-only by default.
171171

172172
.. php:attr:: actual
173173

docs/persistence.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -843,7 +843,7 @@ SQL Actions on Linked Records
843843
-----------------------------
844844

845845
In conjunction with Model::refLink() you can produce expressions for creating
846-
sub-selects. The functionality is nicely wrapped inside FieldSql_Many::addField()::
846+
sub-selects. The functionality is nicely wrapped inside HasMany::addField()::
847847

848848
$client->hasMany('Invoice')
849849
->addField('total_gross', ['aggregate' => 'sum', 'field' => 'gross']);

docs/persistence/csv.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Loading and Saving CSV Files
88
.. php:class:: Persistence\Csv
99
1010
Agile Data can operate with CSV files for data loading, or saving. The capabilities
11-
of Persistence\Csv are limited to the following actions:
11+
of `Persistence\Csv` are limited to the following actions:
1212

1313
- open any CSV file, use column mapping
1414
- identify which column is corresponding for respective field

docs/sql.rst

+6-6
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@ In addition to normal operations you can extend and customize various queries.
1313
Default Model Classes
1414
=====================
1515

16-
When using Persistence\Sql model building will use different classes for fields,
16+
When using `Persistence\Sql` model building will use different classes for fields,
1717
expressions, joins etc:
1818

1919
- addField - :php:class:`FieldSql` (field can be used as part of DSQL Expression)
2020
- hasOne - :php:class:`Reference\HasOneSql` (allow importing fields)
21-
- addExpression - :php:class:`FieldSqlExpression` (define expression through DSQL)
21+
- addExpression - :php:class:`SqlExpressionField` (define expression through DSQL)
2222
- join - :php:class:`Join\Sql` (join tables query-time)
2323

2424

@@ -134,7 +134,7 @@ SQL Reference
134134
Expressions
135135
-----------
136136

137-
.. php:class:: FieldSqlExpression
137+
.. php:class:: SqlExpressionField
138138
139139
Extends :php:class:`FieldSql`
140140

@@ -193,7 +193,7 @@ Custom Expressions
193193
.. php:method:: expr
194194
195195
This method is also injected into the model, that is associated with
196-
Persistence\Sql so the most convenient way to use this method is by calling
196+
`Persistence\Sql` so the most convenient way to use this method is by calling
197197
`$model->expr('foo')`.
198198

199199
This method is quite similar to \Atk4\Data\Persistence\Sql\Query::expr() method explained here:
@@ -212,7 +212,7 @@ field expressions will be automatically substituted. Here is long / short format
212212

213213
$q = $m->expr('[age] + [birth_year']);
214214

215-
This method is automatically used by :php:class:`FieldSqlExpression`.
215+
This method is automatically used by :php:class:`SqlExpressionField`.
216216

217217

218218
Actions
@@ -429,7 +429,7 @@ as an Action
429429

430430
.. important:: Not all SQL vendors may support this approach.
431431

432-
Method :php:meth:`Persistence\\Sql::action` and :php:meth:`Model::action`
432+
Method :php:meth:`Persistence\Sql::action` and :php:meth:`Model::action`
433433
generates queries for most of model operations. By re-defining this method,
434434
you can significantly affect the query building of an SQL model::
435435

docs/static.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Static Persistence
77

88
.. php:class:: Persistence\Static_
99
10-
Static Persistence extends :php:class:`Persistence\\Array_` to implement
10+
Static Persistence extends :php:class:`Persistence\Array_` to implement
1111
a user-friendly way of specifying data through an array.
1212

1313
Usage

docs/types.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ inside a Table or Form and can be exported through RestAPI::
2323

2424
We also allow use of custom Field implementation::
2525

26-
$this->addField('encrypted_password', new \Atk4\Login\Field\Password());
26+
$this->addField('encrypted_password', new \Atk4\Data\Field\PasswordField());
2727

2828
A properly implemented type will still be able to offer some means to present
2929
it in human-readable format, however in some cases, if you plan on using ATK UI,

phpstan.neon.dist

+8-3
Original file line numberDiff line numberDiff line change
@@ -25,19 +25,24 @@ parameters:
2525
ignoreErrors:
2626
- '~^Unsafe usage of new static\(\)\.$~'
2727

28+
-
29+
message: '~^Call to deprecated method getRawDataByTable\(\) of class Atk4\\Data\\Persistence\\Array_:~'
30+
path: '*'
31+
count: 2
32+
2833
# for Doctrine DBAL 2.x, remove the support once Doctrine ORM 2.10 is released
2934
# see https://github.com/doctrine/orm/issues/8526
3035
-
31-
message: '~^(Call to an undefined method Doctrine\\DBAL\\Driver\\Connection::getWrappedConnection\(\)\.|Caught class Doctrine\\DBAL\\DBALException not found\.|Call to static method notSupported\(\) on an unknown class Doctrine\\DBAL\\DBALException\.|Access to an undefined property Doctrine\\DBAL\\Driver\\PDO\\Connection::\$connection\.|Parameter #1 \$dsn of class Doctrine\\DBAL\\Driver\\PDO\\SQLSrv\\Connection constructor expects string, Doctrine\\DBAL\\Driver\\PDO\\Connection given\.|Method Atk4\\Data\\Persistence\\Sql\\Expression::execute\(\) should return Doctrine\\DBAL\\Result\|PDOStatement but returns bool\.|Class Doctrine\\DBAL\\Platforms\\MySqlPlatform referenced with incorrect case: Doctrine\\DBAL\\Platforms\\MySQLPlatform\.)$~'
36+
message: '~^(Call to an undefined method Doctrine\\DBAL\\Driver\\Connection::getWrappedConnection\(\)\.|Call to an undefined method Doctrine\\DBAL\\Connection::createSchemaManager\(\)\.|Call to an undefined static method Doctrine\\DBAL\\Exception::invalidPdoInstance\(\)\.|Call to deprecated method fetch(|All)\(\) of class Doctrine\\DBAL\\Result:\n.+|Call to deprecated method getSchemaManager\(\) of class Doctrine\\DBAL\\Connection:\n.+|Access to an undefined property Doctrine\\DBAL\\Driver\\PDO\\Connection::\$connection\.|Parameter #1 \$dsn of class Doctrine\\DBAL\\Driver\\PDO\\SQLSrv\\Connection constructor expects string, Doctrine\\DBAL\\Driver\\PDO\\Connection given\.|Method Atk4\\Data\\Persistence\\Sql\\Expression::execute\(\) should return Doctrine\\DBAL\\Result\|PDOStatement but returns bool\.|Class Doctrine\\DBAL\\Platforms\\MySqlPlatform referenced with incorrect case: Doctrine\\DBAL\\Platforms\\MySQLPlatform\.)$~'
3237
path: '*'
3338
# count for DBAL 3.x matched in "src/Persistence/GenericPlatform.php" file
34-
count: 12
39+
count: 13
3540

3641
# TODO these rules are generated, this ignores should be fixed in the code
3742
# for src/Schema/TestCase.php
3843
- '~^Access to an undefined property Atk4\\Data\\Persistence::\$connection\.$~'
3944
- '~^Call to an undefined method Atk4\\Data\\Persistence::dsql\(\)\.$~'
40-
# for src/FieldSqlExpression.php
45+
# for src/Field/SqlExpressionField.php
4146
- '~^Call to an undefined method Atk4\\Data\\Model::expr\(\)\.$~'
4247
# for src/Model.php
4348
- '~^Call to an undefined method Atk4\\Data\\Persistence::update\(\)\.$~'

src/Exception.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
namespace Atk4\Data;
66

7-
class Exception extends \Atk4\Core\Exception
7+
use Atk4\Core\Exception as BaseException;
8+
9+
class Exception extends BaseException
810
{
911
}

src/Field.php

+4-3
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,9 @@ static function (Model $entity) use ($name): self {
9595
*/
9696
private function normalizeUsingTypecast($value)
9797
{
98-
$persistence = $this->getOwner()->persistence
99-
?? new class() extends Persistence {
98+
$persistence = $this->issetOwner() && $this->getOwner()->persistence !== null
99+
? $this->getOwner()->persistence
100+
: new class() extends Persistence {
100101
public function __construct()
101102
{
102103
}
@@ -132,7 +133,7 @@ public function normalize($value)
132133
$this->getTypeObject(); // assert type exists
133134

134135
try {
135-
if ($this->getOwner()->hook(Model::HOOK_NORMALIZE, [$this, $value]) === false) {
136+
if ($this->issetOwner() && $this->getOwner()->hook(Model::HOOK_NORMALIZE, [$this, $value]) === false) {
136137
return $value;
137138
}
138139

src/Field/Callback.php src/Field/CallbackField.php

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@
55
namespace Atk4\Data\Field;
66

77
use Atk4\Core\InitializerTrait;
8+
use Atk4\Data\Field;
89
use Atk4\Data\Model;
910

1011
/**
1112
* Evaluate php expression after load.
1213
*/
13-
class Callback extends \Atk4\Data\Field
14+
class CallbackField extends Field
1415
{
1516
use InitializerTrait {
1617
init as private _init;

src/Field/Email.php

-106
This file was deleted.

src/Field/EmailField.php

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Atk4\Data\Field;
6+
7+
use Atk4\Data\Field;
8+
use Atk4\Data\ValidationException;
9+
10+
/**
11+
* Stores valid email as per configuration.
12+
*
13+
* Usage:
14+
* $user->addField('email', [EmailField::class]);
15+
* $user->addField('email_mx_check', [EmailField::class, 'dns_check' => true]);
16+
* $user->addField('email_with_name', [EmailField::class, 'allow_name' => true]);
17+
*/
18+
class EmailField extends Field
19+
{
20+
/** @var bool Enable lookup for MX record for email addresses stored */
21+
public $dns_check = false;
22+
23+
/** @var bool Allow display name as per RFC2822, eg. format like "Romans <me@example.com>" */
24+
public $allow_name = false;
25+
26+
public function normalize($value)
27+
{
28+
$value = parent::normalize($value);
29+
if ($value === null) {
30+
return $value;
31+
}
32+
33+
$email = trim($value);
34+
if ($this->allow_name) {
35+
$email = preg_replace('/^[^<]*<([^>]*)>/', '\1', $email);
36+
}
37+
38+
if (strpos($email, '@') === false) {
39+
throw new ValidationException([$this->name => 'Email address does not have domain'], $this->getOwner());
40+
}
41+
42+
[$user, $domain] = explode('@', $email, 2);
43+
$domain = idn_to_ascii($domain, \IDNA_DEFAULT, \INTL_IDNA_VARIANT_UTS46); // always convert domain to ASCII
44+
45+
if (!filter_var($user . '@' . $domain, \FILTER_VALIDATE_EMAIL)) {
46+
throw new ValidationException([$this->name => 'Email address format is invalid'], $this->getOwner());
47+
}
48+
49+
if ($this->dns_check) {
50+
if (!$this->hasAnyDnsRecord($domain)) {
51+
throw new ValidationException([$this->name => 'Email address domain does not exist'], $this->getOwner());
52+
}
53+
}
54+
55+
return parent::normalize($value);
56+
}
57+
58+
private function hasAnyDnsRecord(string $domain, array $types = ['MX', 'A', 'AAAA', 'CNAME']): bool
59+
{
60+
foreach (array_unique(array_map('strtoupper', $types)) as $t) {
61+
$dnsConsts = [
62+
'MX' => \DNS_MX,
63+
'A' => \DNS_A,
64+
'AAAA' => \DNS_AAAA,
65+
'CNAME' => \DNS_CNAME,
66+
];
67+
68+
$records = @dns_get_record($domain . '.', $dnsConsts[$t]);
69+
if ($records === false) { // retry once on failure
70+
$records = dns_get_record($domain . '.', $dnsConsts[$t]);
71+
}
72+
if ($records !== false && count($records) > 0) {
73+
return true;
74+
}
75+
}
76+
77+
return false;
78+
}
79+
}

0 commit comments

Comments
 (0)