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 Password Field, drop multiple support in Email Field #924

Merged
merged 18 commits into from
Nov 23, 2021
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ Each persistence implements actions differently. SQL is probably the most full-f

### Introducing Expressions

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:
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:

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

Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"johnkary/phpunit-speedtrap": "^3.3",
"phpstan/extension-installer": "^1.1",
"phpstan/phpstan": "^1.0",
"phpstan/phpstan-deprecation-rules": "^1.0",
"phpunit/phpunit": "^9.5.5"
},
"suggest": {
Expand Down
4 changes: 2 additions & 2 deletions docs/fields.rst
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ Conversions between types is what we call :ref:`Typecasting` and there is a
documentation section dedicated to it.

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

Valid types are: string, integer, boolean, datetime, date, time.
Expand Down Expand Up @@ -167,7 +167,7 @@ Example::
.. php:attr:: read_only

Modifying field that is read-only through set() methods (or array access) will
result in exception. :php:class:`FieldSqlExpression` is read-only by default.
result in exception. :php:class:`SqlExpressionField` is read-only by default.

.. php:attr:: actual

Expand Down
2 changes: 1 addition & 1 deletion docs/persistence.rst
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,7 @@ SQL Actions on Linked Records
-----------------------------

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

$client->hasMany('Invoice')
->addField('total_gross', ['aggregate' => 'sum', 'field' => 'gross']);
Expand Down
2 changes: 1 addition & 1 deletion docs/persistence/csv.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Loading and Saving CSV Files
.. php:class:: Persistence\Csv

Agile Data can operate with CSV files for data loading, or saving. The capabilities
of Persistence\Csv are limited to the following actions:
of `Persistence\Csv` are limited to the following actions:

- open any CSV file, use column mapping
- identify which column is corresponding for respective field
Expand Down
12 changes: 6 additions & 6 deletions docs/sql.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ In addition to normal operations you can extend and customize various queries.
Default Model Classes
=====================

When using Persistence\Sql model building will use different classes for fields,
When using `Persistence\Sql` model building will use different classes for fields,
expressions, joins etc:

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


Expand Down Expand Up @@ -134,7 +134,7 @@ SQL Reference
Expressions
-----------

.. php:class:: FieldSqlExpression
.. php:class:: SqlExpressionField

Extends :php:class:`FieldSql`

Expand Down Expand Up @@ -193,7 +193,7 @@ Custom Expressions
.. php:method:: expr

This method is also injected into the model, that is associated with
Persistence\Sql so the most convenient way to use this method is by calling
`Persistence\Sql` so the most convenient way to use this method is by calling
`$model->expr('foo')`.

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

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

This method is automatically used by :php:class:`FieldSqlExpression`.
This method is automatically used by :php:class:`SqlExpressionField`.


Actions
Expand Down Expand Up @@ -429,7 +429,7 @@ as an Action

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

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

Expand Down
2 changes: 1 addition & 1 deletion docs/static.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Static Persistence

.. php:class:: Persistence\Static_

Static Persistence extends :php:class:`Persistence\\Array_` to implement
Static Persistence extends :php:class:`Persistence\Array_` to implement
a user-friendly way of specifying data through an array.

Usage
Expand Down
2 changes: 1 addition & 1 deletion docs/types.rst
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ inside a Table or Form and can be exported through RestAPI::

We also allow use of custom Field implementation::

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

A properly implemented type will still be able to offer some means to present
it in human-readable format, however in some cases, if you plan on using ATK UI,
Expand Down
11 changes: 8 additions & 3 deletions phpstan.neon.dist
Original file line number Diff line number Diff line change
Expand Up @@ -25,19 +25,24 @@ parameters:
ignoreErrors:
- '~^Unsafe usage of new static\(\)\.$~'

-
message: '~^Call to deprecated method getRawDataByTable\(\) of class Atk4\\Data\\Persistence\\Array_:~'
path: '*'
count: 2

# for Doctrine DBAL 2.x, remove the support once Doctrine ORM 2.10 is released
# see https://github.com/doctrine/orm/issues/8526
-
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\.)$~'
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\.)$~'
path: '*'
# count for DBAL 3.x matched in "src/Persistence/GenericPlatform.php" file
count: 12
count: 13

# TODO these rules are generated, this ignores should be fixed in the code
# for src/Schema/TestCase.php
- '~^Access to an undefined property Atk4\\Data\\Persistence::\$connection\.$~'
- '~^Call to an undefined method Atk4\\Data\\Persistence::dsql\(\)\.$~'
# for src/FieldSqlExpression.php
# for src/Field/SqlExpressionField.php
- '~^Call to an undefined method Atk4\\Data\\Model::expr\(\)\.$~'
# for src/Model.php
- '~^Call to an undefined method Atk4\\Data\\Persistence::update\(\)\.$~'
Expand Down
4 changes: 3 additions & 1 deletion src/Exception.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

namespace Atk4\Data;

class Exception extends \Atk4\Core\Exception
use Atk4\Core\Exception as BaseException;

class Exception extends BaseException
{
}
7 changes: 4 additions & 3 deletions src/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,9 @@ static function (Model $entity) use ($name): self {
*/
private function normalizeUsingTypecast($value)
{
$persistence = $this->getOwner()->persistence
?? new class() extends Persistence {
$persistence = $this->issetOwner() && $this->getOwner()->persistence !== null
? $this->getOwner()->persistence
: new class() extends Persistence {
public function __construct()
{
}
Expand Down Expand Up @@ -132,7 +133,7 @@ public function normalize($value)
$this->getTypeObject(); // assert type exists

try {
if ($this->getOwner()->hook(Model::HOOK_NORMALIZE, [$this, $value]) === false) {
if ($this->issetOwner() && $this->getOwner()->hook(Model::HOOK_NORMALIZE, [$this, $value]) === false) {
return $value;
}

Expand Down
3 changes: 2 additions & 1 deletion src/Field/Callback.php → src/Field/CallbackField.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@
namespace Atk4\Data\Field;

use Atk4\Core\InitializerTrait;
use Atk4\Data\Field;
use Atk4\Data\Model;

/**
* Evaluate php expression after load.
*/
class Callback extends \Atk4\Data\Field
class CallbackField extends Field
{
use InitializerTrait {
init as private _init;
Expand Down
106 changes: 0 additions & 106 deletions src/Field/Email.php

This file was deleted.

79 changes: 79 additions & 0 deletions src/Field/EmailField.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
<?php

declare(strict_types=1);

namespace Atk4\Data\Field;

use Atk4\Data\Field;
use Atk4\Data\ValidationException;

/**
* Stores valid email as per configuration.
*
* Usage:
* $user->addField('email', [EmailField::class]);
* $user->addField('email_mx_check', [EmailField::class, 'dns_check' => true]);
* $user->addField('email_with_name', [EmailField::class, 'allow_name' => true]);
*/
class EmailField extends Field
{
/** @var bool Enable lookup for MX record for email addresses stored */
public $dns_check = false;

/** @var bool Allow display name as per RFC2822, eg. format like "Romans <[email protected]>" */
public $allow_name = false;

public function normalize($value)
{
$value = parent::normalize($value);
if ($value === null) {
return $value;
}

$email = trim($value);
if ($this->allow_name) {
$email = preg_replace('/^[^<]*<([^>]*)>/', '\1', $email);
}

if (strpos($email, '@') === false) {
throw new ValidationException([$this->name => 'Email address does not have domain'], $this->getOwner());
}

[$user, $domain] = explode('@', $email, 2);
$domain = idn_to_ascii($domain, \IDNA_DEFAULT, \INTL_IDNA_VARIANT_UTS46); // always convert domain to ASCII

if (!filter_var($user . '@' . $domain, \FILTER_VALIDATE_EMAIL)) {
throw new ValidationException([$this->name => 'Email address format is invalid'], $this->getOwner());
}

if ($this->dns_check) {
if (!$this->hasAnyDnsRecord($domain)) {
throw new ValidationException([$this->name => 'Email address domain does not exist'], $this->getOwner());
}
}

return parent::normalize($value);
}

private function hasAnyDnsRecord(string $domain, array $types = ['MX', 'A', 'AAAA', 'CNAME']): bool
{
foreach (array_unique(array_map('strtoupper', $types)) as $t) {
$dnsConsts = [
'MX' => \DNS_MX,
'A' => \DNS_A,
'AAAA' => \DNS_AAAA,
'CNAME' => \DNS_CNAME,
];

$records = @dns_get_record($domain . '.', $dnsConsts[$t]);
if ($records === false) { // retry once on failure
$records = dns_get_record($domain . '.', $dnsConsts[$t]);
}
if ($records !== false && count($records) > 0) {
return true;
}
}

return false;
}
}
Loading