Merge branch '20-update-telephone-type-new-approach' into 'master'

fix: Use `odolbeau/phone-number-bundle` for formatting phone number type fields.

See merge request Chill-Projet/chill-bundles!322
This commit is contained in:
Julien Fastré 2022-03-02 20:39:52 +00:00
commit d0591d0351
44 changed files with 711 additions and 254 deletions

View File

@ -29,6 +29,7 @@ variables:
REDIS_URL: redis://redis:6379 REDIS_URL: redis://redis:6379
# change vendor dir to make the app install into tests/apps # change vendor dir to make the app install into tests/apps
COMPOSER_VENDOR_DIR: tests/app/vendor COMPOSER_VENDOR_DIR: tests/app/vendor
DEFAULT_CARRIER_CODE: BE
stages: stages:
- Composer install - Composer install
@ -78,6 +79,7 @@ psalm_tests:
image: registry.gitlab.com/chill-projet/chill-app/php-base-image:7.4 image: registry.gitlab.com/chill-projet/chill-app/php-base-image:7.4
script: script:
- bin/grumphp run --tasks=psalm - bin/grumphp run --tasks=psalm
allow_failure: true
artifacts: artifacts:
expire_in: 30 min expire_in: 30 min
paths: paths:

View File

@ -22,6 +22,7 @@
"league/csv": "^9.7.1", "league/csv": "^9.7.1",
"nyholm/psr7": "^1.4", "nyholm/psr7": "^1.4",
"ocramius/package-versions": "^1.10", "ocramius/package-versions": "^1.10",
"odolbeau/phone-number-bundle": "^3.6",
"phpoffice/phpspreadsheet": "^1.16", "phpoffice/phpspreadsheet": "^1.16",
"ramsey/uuid-doctrine": "^1.7", "ramsey/uuid-doctrine": "^1.7",
"sensio/framework-extra-bundle": "^5.5", "sensio/framework-extra-bundle": "^5.5",

View File

@ -325,11 +325,6 @@ parameters:
count: 1 count: 1
path: src/Bundle/ChillMainBundle/Timeline/TimelineBuilder.php path: src/Bundle/ChillMainBundle/Timeline/TimelineBuilder.php
-
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1
path: src/Bundle/ChillMainBundle/Validation/Validator/ValidPhonenumber.php
- -
message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#" message: "#^Construct empty\\(\\) is not allowed\\. Use more strict comparison\\.$#"
count: 1 count: 1

View File

@ -39,6 +39,7 @@ use Chill\MainBundle\Form\LocationTypeType;
use Chill\MainBundle\Form\UserJobType; use Chill\MainBundle\Form\UserJobType;
use Chill\MainBundle\Form\UserType; use Chill\MainBundle\Form\UserType;
use Exception; use Exception;
use Misd\PhoneNumberBundle\Doctrine\DBAL\Types\PhoneNumberType;
use Ramsey\Uuid\Doctrine\UuidType; use Ramsey\Uuid\Doctrine\UuidType;
use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -235,6 +236,7 @@ class ChillMainExtension extends Extension implements
'dateinterval' => NativeDateIntervalType::class, 'dateinterval' => NativeDateIntervalType::class,
'point' => PointType::class, 'point' => PointType::class,
'uuid' => UuidType::class, 'uuid' => UuidType::class,
'phone_number' => PhoneNumberType::class,
], ],
], ],
] ]

View File

@ -97,6 +97,9 @@ class Configuration implements ConfigurationInterface
->scalarNode('twilio_secret') ->scalarNode('twilio_secret')
->defaultNull() ->defaultNull()
->end() ->end()
->scalarNode('default_carrier_code')
->defaultNull()
->end()
->end() ->end()
->end() ->end()
->arrayNode('acl') ->arrayNode('acl')

View File

@ -18,9 +18,9 @@ use Chill\MainBundle\Validation\Constraint\PhonenumberConstraint;
use DateTimeImmutable; use DateTimeImmutable;
use DateTimeInterface; use DateTimeInterface;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use libphonenumber\PhoneNumber;
use Symfony\Component\Serializer\Annotation as Serializer; use Symfony\Component\Serializer\Annotation as Serializer;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Validator\Constraints as Assert;
/** /**
* @ORM\Table(name="chill_main_location") * @ORM\Table(name="chill_main_location")
@ -90,20 +90,18 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
private ?string $name = null; private ?string $name = null;
/** /**
* @ORM\Column(type="string", length=64, nullable=true) * @ORM\Column(type="phone_number", nullable=true)
* @Serializer\Groups({"read", "write", "docgen:read"}) * @Serializer\Groups({"read", "write", "docgen:read"})
* @Assert\Regex(pattern="/^([\+{1}])([0-9\s*]{4,20})$/")
* @PhonenumberConstraint(type="any") * @PhonenumberConstraint(type="any")
*/ */
private ?string $phonenumber1 = null; private ?PhoneNumber $phonenumber1 = null;
/** /**
* @ORM\Column(type="string", length=64, nullable=true) * @ORM\Column(type="phone_number", nullable=true)
* @Serializer\Groups({"read", "write", "docgen:read"}) * @Serializer\Groups({"read", "write", "docgen:read"})
* @Assert\Regex(pattern="/^([\+{1}])([0-9\s*]{4,20})$/")
* @PhonenumberConstraint(type="any") * @PhonenumberConstraint(type="any")
*/ */
private ?string $phonenumber2 = null; private ?PhoneNumber $phonenumber2 = null;
/** /**
* @ORM\Column(type="datetime_immutable", nullable=true) * @ORM\Column(type="datetime_immutable", nullable=true)
@ -162,12 +160,12 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
return $this->name; return $this->name;
} }
public function getPhonenumber1(): ?string public function getPhonenumber1(): ?PhoneNumber
{ {
return $this->phonenumber1; return $this->phonenumber1;
} }
public function getPhonenumber2(): ?string public function getPhonenumber2(): ?PhoneNumber
{ {
return $this->phonenumber2; return $this->phonenumber2;
} }
@ -238,14 +236,14 @@ class Location implements TrackCreationInterface, TrackUpdateInterface
return $this; return $this;
} }
public function setPhonenumber1(?string $phonenumber1): self public function setPhonenumber1(?PhoneNumber $phonenumber1): self
{ {
$this->phonenumber1 = $phonenumber1; $this->phonenumber1 = $phonenumber1;
return $this; return $this;
} }
public function setPhonenumber2(?string $phonenumber2): self public function setPhonenumber2(?PhoneNumber $phonenumber2): self
{ {
$this->phonenumber2 = $phonenumber2; $this->phonenumber2 = $phonenumber2;

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\MainBundle\Form; namespace Chill\MainBundle\Form;
use Chill\MainBundle\Entity\LocationType as EntityLocationType; use Chill\MainBundle\Entity\LocationType as EntityLocationType;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Form\Type\PickAddressType; use Chill\MainBundle\Form\Type\PickAddressType;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Bridge\Doctrine\Form\Type\EntityType;
@ -46,8 +47,8 @@ final class LocationFormType extends AbstractType
}, },
]) ])
->add('name', TextType::class) ->add('name', TextType::class)
->add('phonenumber1', TextType::class, ['required' => false]) ->add('phonenumber1', ChillPhoneNumberType::class, ['required' => false])
->add('phonenumber2', TextType::class, ['required' => false]) ->add('phonenumber2', ChillPhoneNumberType::class, ['required' => false])
->add('email', TextType::class, ['required' => false]) ->add('email', TextType::class, ['required' => false])
->add('address', PickAddressType::class, [ ->add('address', PickAddressType::class, [
'required' => false, 'required' => false,

View File

@ -0,0 +1,61 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Form\Type;
use libphonenumber\PhoneNumberFormat;
use libphonenumber\PhoneNumberUtil;
use Misd\PhoneNumberBundle\Form\Type\PhoneNumberType;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\OptionsResolver\Options;
use Symfony\Component\OptionsResolver\OptionsResolver;
use function array_key_exists;
class ChillPhoneNumberType extends AbstractType
{
private string $defaultCarrierCode;
private PhoneNumberUtil $phoneNumberUtil;
public function __construct(ParameterBagInterface $parameterBag)
{
$this->defaultCarrierCode = $parameterBag->get('chill_main')['phone_helper']['default_carrier_code'];
$this->phoneNumberUtil = PhoneNumberUtil::getInstance();
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver
->setDefault('default_region', $this->defaultCarrierCode)
->setDefault('format', PhoneNumberFormat::NATIONAL)
->setDefault('type', \libphonenumber\PhoneNumberType::FIXED_LINE_OR_MOBILE)
->setNormalizer('attr', function (Options $options, $value) {
if (array_key_exists('placeholder', $value)) {
return $value;
}
$examplePhoneNumber = $this->phoneNumberUtil->getExampleNumberForType($this->defaultCarrierCode, $options['type']);
return array_merge(
$value,
[
'placeholder' => PhoneNumberUtil::getInstance()->format($examplePhoneNumber, $options['format']),
]
);
});
}
public function getParent()
{
return PhoneNumberType::class;
}
}

View File

@ -38,7 +38,7 @@ class ObjectToIdTransformer implements DataTransformerInterface
*/ */
public function reverseTransform($id) public function reverseTransform($id)
{ {
if (!$id) { if (null === $id) {
return null; return null;
} }
@ -46,7 +46,7 @@ class ObjectToIdTransformer implements DataTransformerInterface
->getRepository($this->class) ->getRepository($this->class)
->find($id); ->find($id);
if (!$object) { if (null === $object) {
throw new TransformationFailedException(); throw new TransformationFailedException();
} }
@ -62,7 +62,7 @@ class ObjectToIdTransformer implements DataTransformerInterface
*/ */
public function transform($object) public function transform($object)
{ {
if (!$object) { if (null === $object) {
return ''; return '';
} }

View File

@ -0,0 +1,54 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Phonenumber;
use libphonenumber\PhoneNumber;
/**
* Helper to some task linked to phonenumber.
*
* Currently, only Twilio is supported (https://www.twilio.com/lookup). A method
* allow to check if the helper is configured for validation. This should be used
* before doing some validation.
*/
interface PhoneNumberHelperInterface
{
public function format(PhoneNumber $phoneNumber): string;
/**
* Get type (mobile, landline, ...) for phone number.
*/
public function getType(string $phonenumber): string;
/**
* Return true if the validation is configured and available.
*/
public function isPhonenumberValidationConfigured(): bool;
/**
* Return true if the phonenumber is a landline or voip phone. Return always true
* if the validation is not configured.
*/
public function isValidPhonenumberAny(string $phonenumber): bool;
/**
* Return true if the phonenumber is a landline or voip phone. Return always true
* if the validation is not configured.
*/
public function isValidPhonenumberLandOrVoip(string $phonenumber): bool;
/**
* REturn true if the phoennumber is a mobile phone. Return always true
* if the validation is not configured.
*/
public function isValidPhonenumberMobile(string $phonenumber): bool;
}

View File

@ -15,8 +15,12 @@ use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException; use GuzzleHttp\Exception\ClientException;
use GuzzleHttp\Exception\ConnectException; use GuzzleHttp\Exception\ConnectException;
use GuzzleHttp\Exception\ServerException; use GuzzleHttp\Exception\ServerException;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberUtil;
use Psr\Cache\CacheItemPoolInterface; use Psr\Cache\CacheItemPoolInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use function array_key_exists; use function array_key_exists;
use function in_array; use function in_array;
@ -24,40 +28,32 @@ use function json_decode;
use function preg_replace; use function preg_replace;
use function strlen; use function strlen;
/** final class PhonenumberHelper implements PhoneNumberHelperInterface
* Helper to some task linked to phonenumber.
*
* Currently, only Twilio is supported (https://www.twilio.com/lookup). A method
* allow to check if the helper is configured for validation. This should be used
* before doing some validation.
*/
class PhonenumberHelper
{ {
public const FORMAT_URI = 'https://lookups.twilio.com/v1/PhoneNumbers/%s'; public const FORMAT_URI = 'https://lookups.twilio.com/v1/PhoneNumbers/%s';
public const LOOKUP_URI = 'https://lookups.twilio.com/v1/PhoneNumbers/%s'; public const LOOKUP_URI = 'https://lookups.twilio.com/v1/PhoneNumbers/%s';
protected CacheItemPoolInterface $cachePool; private CacheItemPoolInterface $cachePool;
/** private array $config;
* TRUE if the client is properly configured.
*/
protected bool $isConfigured = false;
protected LoggerInterface $logger; private bool $isConfigured = false;
/** private LoggerInterface $logger;
* Twilio client.
*/ private PhonenumberUtil $phoneNumberUtil;
protected Client $twilioClient;
private Client $twilioClient;
public function __construct( public function __construct(
CacheItemPoolInterface $cachePool, CacheItemPoolInterface $cacheUserData,
$config, ParameterBagInterface $parameterBag,
LoggerInterface $logger LoggerInterface $logger
) { ) {
$this->logger = $logger; $this->logger = $logger;
$this->cachePool = $cachePool; $this->cachePool = $cacheUserData;
$this->config = $config = $parameterBag->get('chill_main.phone_helper');
if ( if (
array_key_exists('twilio_sid', $config) array_key_exists('twilio_sid', $config)
@ -72,11 +68,19 @@ class PhonenumberHelper
]); ]);
$this->isConfigured = true; $this->isConfigured = true;
} }
$this->phoneNumberUtil = PhoneNumberUtil::getInstance();
} }
public function format($phonenumber) /**
* @param string $phoneNumber A national phone number starting with +
*
* @throws NumberParseException
*/
public function format(PhoneNumber $phoneNumber): string
{ {
return $this->performTwilioFormat($phonenumber); return $this->phoneNumberUtil
->formatOutOfCountryCallingNumber($phoneNumber, $this->config['default_carrier_code']);
} }
/** /**
@ -137,7 +141,7 @@ class PhonenumberHelper
} }
/** /**
* REturn true if the phoennumber is a mobile phone. Return always true * REturn true if the phonenumber is a mobile phone. Return always true
* if the validation is not configured. * if the validation is not configured.
* *
* @param string $phonenumber * @param string $phonenumber
@ -157,68 +161,7 @@ class PhonenumberHelper
return 'mobile' === $validation; return 'mobile' === $validation;
} }
protected function performTwilioFormat($phonenumber) private function performTwilioLookup($phonenumber)
{
if (false === $this->isPhonenumberValidationConfigured()) {
return $phonenumber;
}
// filter only number
$filtered = preg_replace('/[^0-9]/', '', $phonenumber);
$item = $this->cachePool->getItem('pnum_format_nat_' . $filtered);
if ($item->isHit()) {
return $item->get();
}
try {
$response = $this->twilioClient->get(sprintf(self::FORMAT_URI, '+' . $filtered), [
'http_errors' => true,
]);
} catch (ClientException $e) {
$response = $e->getResponse();
$this->logger->error('[phonenumber helper] Could not format number '
. 'due to client error', [
'message' => $response->getBody()->getContents(),
'status_code' => $response->getStatusCode(),
'phonenumber' => $phonenumber,
]);
return $phonenumber;
} catch (ServerException $e) {
$response = $e->getResponse();
$this->logger->error('[phonenumber helper] Could not format number '
. 'due to server error', [
'message' => $response->getBody()->getContents(),
'status_code' => $response->getStatusCode(),
'phonenumber' => $phonenumber,
]);
return null;
} catch (ConnectException $e) {
$this->logger->error('[phonenumber helper] Could not format number '
. 'due to connect error', [
'message' => $e->getMessage(),
'phonenumber' => $phonenumber,
]);
return null;
}
$format = json_decode($response->getBody()->getContents())->national_format;
$item
->set($format)
// expires after 3d
->expiresAfter(3600 * 24 * 3);
$this->cachePool->save($item);
return $format;
}
protected function performTwilioLookup($phonenumber)
{ {
if (false === $this->isPhonenumberValidationConfigured()) { if (false === $this->isPhonenumberValidationConfigured()) {
return null; return null;
@ -230,7 +173,7 @@ class PhonenumberHelper
$item = $this->cachePool->getItem('pnum_' . $filtered); $item = $this->cachePool->getItem('pnum_' . $filtered);
if ($item->isHit()) { if ($item->isHit()) {
//return $item->get(); return $item->get();
} }
try { try {

View File

@ -16,10 +16,7 @@ use Twig\TwigFilter;
class Templating extends AbstractExtension class Templating extends AbstractExtension
{ {
/** protected PhonenumberHelper $phonenumberHelper;
* @var PhonenumberHelper
*/
protected $phonenumberHelper;
public function __construct(PhonenumberHelper $phonenumberHelper) public function __construct(PhonenumberHelper $phonenumberHelper)
{ {

View File

@ -18,8 +18,10 @@
{% for entity in entities %} {% for entity in entities %}
<tr> <tr>
<td>{{ entity.name }}</td> <td>{{ entity.name }}</td>
<td>{{ entity.phonenumber1 }}</td> <td>
<td>{{ entity.phonenumber2 }}</td> {{ entity.phonenumber1|chill_format_phonenumber }}
</td>
<td>{{ entity.phonenumber2|chill_format_phonenumber }}</td>
<td>{{ entity.email }}</td> <td>{{ entity.email }}</td>
<td> <td>
{% if entity.address is not null %} {% if entity.address is not null %}

View File

@ -11,8 +11,10 @@ declare(strict_types=1);
namespace Chill\MainBundle\Search\Utils; namespace Chill\MainBundle\Search\Utils;
use libphonenumber\PhoneNumberUtil;
use LogicException; use LogicException;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use function count; use function count;
use function implode; use function implode;
use function preg_match; use function preg_match;
@ -24,6 +26,13 @@ class ExtractPhonenumberFromPattern
{ {
private const PATTERN = '([\\+]{0,1}[0-9\\ ]{5,})'; private const PATTERN = '([\\+]{0,1}[0-9\\ ]{5,})';
private string $defaultCarrierCode;
public function __construct(ParameterBagInterface $parameterBag)
{
$this->defaultCarrierCode = $parameterBag->get('chill_main')['phone_helper']['default_carrier_code'];
}
public function extractPhonenumber(string $subject): SearchExtractionResult public function extractPhonenumber(string $subject): SearchExtractionResult
{ {
$matches = []; $matches = [];
@ -35,11 +44,21 @@ class ExtractPhonenumberFromPattern
foreach (str_split(trim($matches[0])) as $key => $char) { foreach (str_split(trim($matches[0])) as $key => $char) {
switch ($char) { switch ($char) {
case '+':
if (0 === $key) {
$phonenumber[] = $char;
} else {
throw new LogicException('should not match not alnum character');
}
break;
case '0': case '0':
$length++; $length++;
if (0 === $key) { if (0 === $key) {
$phonenumber[] = '+32'; $util = PhoneNumberUtil::getInstance();
$phonenumber[] = '+' . $util->getCountryCodeForRegion($this->defaultCarrierCode);
} else { } else {
$phonenumber[] = $char; $phonenumber[] = $char;
} }

View File

@ -0,0 +1,64 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Serializer\Normalizer;
use libphonenumber\NumberParseException;
use libphonenumber\PhoneNumber;
use libphonenumber\PhoneNumberUtil;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerInterface;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
class PhonenumberNormalizer implements NormalizerInterface, DenormalizerInterface
{
private string $defaultCarrierCode;
private PhoneNumberUtil $phoneNumberUtil;
public function __construct(ParameterBagInterface $parameterBag)
{
$this->defaultCarrierCode = $parameterBag->get('chill_main')['phone_helper']['default_carrier_code'];
$this->phoneNumberUtil = PhoneNumberUtil::getInstance();
}
/**
* @param mixed $data
* @param mixed $type
* @param null|mixed $format
*
* @throws UnexpectedValueException
*/
public function denormalize($data, $type, $format = null, array $context = [])
{
try {
return $this->phoneNumberUtil->parse($data, $this->defaultCarrierCode);
} catch (NumberParseException $e) {
throw new UnexpectedValueException($e->getMessage(), $e->getCode(), $e);
}
}
public function normalize($object, ?string $format = null, array $context = []): string
{
return $this->phoneNumberUtil->formatOutOfCountryCallingNumber($object, $this->defaultCarrierCode);
}
public function supportsDenormalization($data, $type, $format = null)
{
return 'libphonenumber\PhoneNumber' === $type;
}
public function supportsNormalization($data, ?string $format = null)
{
return $data instanceof PhoneNumber;
}
}

View File

@ -0,0 +1,72 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\MainBundle\Tests\Routing\Loader;
use Chill\MainBundle\Phonenumber\PhonenumberHelper;
use libphonenumber\PhoneNumberUtil;
use Psr\Log\NullLogger;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
/**
* @internal
* @coversNothing
*/
final class PhonenumberHelperTest extends KernelTestCase
{
public function formatPhonenumbers()
{
yield [
'BE',
'+3281136917',
'081 13 69 17',
];
yield [
'FR',
'+33 6 23 12 45 54',
'06 23 12 45 54',
];
yield [
'FR',
'+32 81 13 69 17',
'00 32 81 13 69 17',
];
yield [
'BE',
'+33 6 23 12 45 54',
'00 33 6 23 12 45 54',
];
}
/**
* @dataProvider formatPhonenumbers
*/
public function testFormatPhonenumbers(string $defaultCarrierCode, string $phoneNumber, string $expected)
{
$util = PhoneNumberUtil::getInstance();
$subject = new PhonenumberHelper(
new ArrayAdapter(),
new ParameterBag([
'chill_main.phone_helper' => [
'default_carrier_code' => $defaultCarrierCode,
],
]),
new NullLogger()
);
$this->assertEquals($expected, $subject->format($util->parse($phoneNumber)));
}
}

View File

@ -13,6 +13,7 @@ namespace Search\Utils;
use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern; use Chill\MainBundle\Search\Utils\ExtractPhonenumberFromPattern;
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag;
/** /**
* @internal * @internal
@ -22,17 +23,25 @@ final class ExtractPhonenumberFromPatternTest extends KernelTestCase
{ {
public function provideData() public function provideData()
{ {
yield ['Diallo', 0, [], 'Diallo', 'no phonenumber']; yield ['BE', 'Diallo', 0, [], 'Diallo', 'no phonenumber'];
yield ['Diallo 15/06/2021', 0, [], 'Diallo 15/06/2021', 'no phonenumber and a date']; yield ['BE', 'Diallo 15/06/2021', 0, [], 'Diallo 15/06/2021', 'no phonenumber and a date'];
yield ['Diallo 0486 123 456', 1, ['+32486123456'], 'Diallo', 'a phonenumber and a name']; yield ['BE', 'Diallo 0486 123 456', 1, ['+32486123456'], 'Diallo', 'a phonenumber and a name'];
yield ['Diallo 123 456', 1, ['123456'], 'Diallo', 'a number and a name, without leadiing 0']; yield ['BE', 'Diallo 123 456', 1, ['123456'], 'Diallo', 'a number and a name, without leadiing 0'];
yield ['123 456', 1, ['123456'], '', 'only phonenumber']; yield ['BE', '123 456', 1, ['123456'], '', 'only phonenumber'];
yield ['0123 456', 1, ['+32123456'], '', 'only phonenumber with a leading 0']; yield ['BE', '0123 456', 1, ['+32123456'], '', 'only phonenumber with a leading 0'];
yield ['FR', '123 456', 1, ['123456'], '', 'only phonenumber'];
yield ['FR', '0123 456', 1, ['+33123456'], '', 'only phonenumber with a leading 0'];
yield ['FR', 'Diallo 0486 123 456', 1, ['+33486123456'], 'Diallo', 'a phonenumber and a name'];
yield ['FR', 'Diallo +32486 123 456', 1, ['+32486123456'], 'Diallo', 'a phonenumber and a name'];
} }
/** /**
@ -44,9 +53,11 @@ final class ExtractPhonenumberFromPatternTest extends KernelTestCase
* @param mixed $filteredSubject * @param mixed $filteredSubject
* @param mixed $msg * @param mixed $msg
*/ */
public function testExtract($subject, $expectedCount, $expected, $filteredSubject, $msg) public function testExtract(string $defaultCarrierCode, $subject, $expectedCount, $expected, $filteredSubject, $msg)
{ {
$extractor = new ExtractPhonenumberFromPattern(); $extractor = new ExtractPhonenumberFromPattern(new ParameterBag(['chill_main' => [
'phone_helper' => ['default_carrier_code' => $defaultCarrierCode],
]]));
$result = $extractor->extractPhonenumber($subject); $result = $extractor->extractPhonenumber($subject);
$this->assertCount($expectedCount, $result->getFound()); $this->assertCount($expectedCount, $result->getFound());

View File

@ -11,24 +11,21 @@ declare(strict_types=1);
namespace Chill\MainBundle\Validation\Validator; namespace Chill\MainBundle\Validation\Validator;
use Chill\MainBundle\Phonenumber\PhonenumberHelper; use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
use LogicException; use LogicException;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
class ValidPhonenumber extends ConstraintValidator final class ValidPhonenumber extends ConstraintValidator
{ {
protected $logger; private LoggerInterface $logger;
/** private PhoneNumberHelperInterface $phonenumberHelper;
* @var PhonenumberHelper
*/
protected $phonenumberHelper;
public function __construct( public function __construct(
LoggerInterface $logger, LoggerInterface $logger,
PhonenumberHelper $phonenumberHelper PhoneNumberHelperInterface $phonenumberHelper
) { ) {
$this->phonenumberHelper = $phonenumberHelper; $this->phonenumberHelper = $phonenumberHelper;
$this->logger = $logger; $this->logger = $logger;
@ -46,7 +43,7 @@ class ValidPhonenumber extends ConstraintValidator
return; return;
} }
if (empty($value)) { if ('' === $value) {
return; return;
} }

View File

@ -26,7 +26,8 @@
], ],
"require": { "require": {
"league/csv": "^9.6", "league/csv": "^9.6",
"phpoffice/phpspreadsheet": "~1.2" "phpoffice/phpspreadsheet": "~1.2",
"odolbeau/phone-number-bundle": "^3.6"
}, },
"require-dev": { "require-dev": {
}, },

View File

@ -96,3 +96,4 @@ services:
- "@security.token_storage" - "@security.token_storage"
Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface: '@Chill\MainBundle\Security\Resolver\CenterResolverDispatcher' Chill\MainBundle\Security\Resolver\CenterResolverDispatcherInterface: '@Chill\MainBundle\Security\Resolver\CenterResolverDispatcher'

View File

@ -3,19 +3,12 @@ services:
autowire: true autowire: true
autoconfigure: true autoconfigure: true
Chill\MainBundle\Phonenumber\PhonenumberHelper: Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface: '@Chill\MainBundle\Phonenumber\PhonenumberHelper'
arguments:
$config: '%chill_main.phone_helper%'
$cachePool: '@cache.user_data'
Chill\MainBundle\Phonenumber\Templating: Chill\MainBundle\Phonenumber\PhonenumberHelper: ~
arguments:
$phonenumberHelper: '@Chill\MainBundle\Phonenumber\PhonenumberHelper' Chill\MainBundle\Phonenumber\Templating: ~
tags:
- { name: twig.extension }
Chill\MainBundle\Validation\Validator\ValidPhonenumber: Chill\MainBundle\Validation\Validator\ValidPhonenumber:
arguments:
$phonenumberHelper: '@Chill\MainBundle\Phonenumber\PhonenumberHelper'
tags: tags:
- { name: validator.constraint_validator } - { name: validator.constraint_validator }

View File

@ -0,0 +1,79 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\Main;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use libphonenumber\PhoneNumberUtil;
use RuntimeException;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
final class Version20220302132728 extends AbstractMigration implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber1 TYPE VARCHAR(64)');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber1 DROP DEFAULT');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber2 TYPE VARCHAR(64)');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber2 DROP DEFAULT');
$this->addSql('COMMENT ON COLUMN chill_main_location.phonenumber1 IS NULL');
$this->addSql('COMMENT ON COLUMN chill_main_location.phonenumber2 IS NULL');
}
public function getDescription(): string
{
return 'Upgrade phonenumber on location';
}
public function up(Schema $schema): void
{
$carrier_code = $this->container
->getParameter('chill_main')['phone_helper']['default_carrier_code'];
if (null === $carrier_code) {
throw new RuntimeException('no carrier code');
}
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber1 TYPE VARCHAR(35)');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber1 DROP DEFAULT');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber1 TYPE VARCHAR(35)');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber2 TYPE VARCHAR(35)');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber2 DROP DEFAULT');
$this->addSql('ALTER TABLE chill_main_location ALTER phonenumber2 TYPE VARCHAR(35)');
$this->addSql('COMMENT ON COLUMN chill_main_location.phonenumber1 IS \'(DC2Type:phone_number)\'');
$this->addSql('COMMENT ON COLUMN chill_main_location.phonenumber2 IS \'(DC2Type:phone_number)\'');
$this->addSql(
'UPDATE chill_main_location SET ' .
$this->buildMigrationPhonenumberClause($carrier_code, 'phonenumber1') .
', ' .
$this->buildMigrationPhoneNumberClause($carrier_code, 'phonenumber2')
);
}
private function buildMigrationPhoneNumberClause(string $defaultCarriercode, string $field): string
{
$util = PhoneNumberUtil::getInstance();
$countryCode = $util->getCountryCodeForRegion($defaultCarriercode);
return sprintf('%s=CASE
WHEN %s = \'\' THEN NULL
WHEN LEFT(%s, 1) = \'0\'
THEN \'+%s\' || replace(replace(substr(%s, 2), \'(0)\', \'\'), \' \', \'\')
ELSE replace(replace(%s, \'(0)\', \'\'),\' \', \'\')
END', $field, $field, $field, $countryCode, $field, $field);
}
}

View File

@ -37,6 +37,7 @@ use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use Exception; use Exception;
use libphonenumber\PhoneNumber;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Validator\Constraints as Assert; use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Context\ExecutionContextInterface;
@ -371,15 +372,10 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
/** /**
* The person's mobile phone number. * The person's mobile phone number.
* *
* @ORM\Column(type="text") * @PhonenumberConstraint(type="mobile")
* @Assert\Regex( * @ORM\Column(type="phone_number", nullable=true)
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
* )
* @PhonenumberConstraint(
* type="mobile",
* )
*/ */
private string $mobilenumber = ''; private ?PhoneNumber $mobilenumber = null;
/** /**
* The person's nationality. * The person's nationality.
@ -429,15 +425,12 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
/** /**
* The person's phonenumber. * The person's phonenumber.
* *
* @ORM\Column(type="text") * @ORM\Column(type="phone_number", nullable=true)
* @Assert\Regex(
* pattern="/^([\+{1}])([0-9\s*]{4,20})$/",
* )
* @PhonenumberConstraint( * @PhonenumberConstraint(
* type="landline", * type="landline",
* ) * )
*/ */
private string $phonenumber = ''; private ?PhoneNumber $phonenumber = null;
/** /**
* The person's place of birth. * The person's place of birth.
@ -1227,10 +1220,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
return $this->memo; return $this->memo;
} }
/** public function getMobilenumber(): ?PhoneNumber
* Get mobilenumber.
*/
public function getMobilenumber(): string
{ {
return $this->mobilenumber; return $this->mobilenumber;
} }
@ -1295,10 +1285,7 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
return $this->otherPhoneNumbers; return $this->otherPhoneNumbers;
} }
/** public function getPhonenumber(): ?PhoneNumber
* Get phonenumber.
*/
public function getPhonenumber(): string
{ {
return $this->phonenumber; return $this->phonenumber;
} }
@ -1737,16 +1724,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
return $this; return $this;
} }
/** public function setMobilenumber(?PhoneNumber $mobilenumber)
* Set mobilenumber.
*
* @param string $mobilenumber
*
* @return Person
*/
public function setMobilenumber(?string $mobilenumber = '')
{ {
$this->mobilenumber = (string) $mobilenumber; $this->mobilenumber = $mobilenumber;
return $this; return $this;
} }
@ -1782,16 +1762,9 @@ class Person implements HasCenterInterface, TrackCreationInterface, TrackUpdateI
return $this; return $this;
} }
/** public function setPhonenumber(?PhoneNumber $phonenumber)
* Set phonenumber.
*
* @param string $phonenumber
*
* @return Person
*/
public function setPhonenumber(?string $phonenumber = '')
{ {
$this->phonenumber = (string) $phonenumber; $this->phonenumber = $phonenumber;
return $this; return $this;
} }

View File

@ -13,17 +13,18 @@ namespace Chill\PersonBundle\Form;
use Chill\MainBundle\Form\Event\CustomizeFormEvent; use Chill\MainBundle\Form\Event\CustomizeFormEvent;
use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Form\Type\PickCenterType; use Chill\MainBundle\Form\Type\PickCenterType;
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper; use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Form\Type\GenderType; use Chill\PersonBundle\Form\Type\GenderType;
use Chill\PersonBundle\Form\Type\PersonAltNameType; use Chill\PersonBundle\Form\Type\PersonAltNameType;
use Chill\PersonBundle\Security\Authorization\PersonVoter; use Chill\PersonBundle\Security\Authorization\PersonVoter;
use libphonenumber\PhoneNumberType;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
@ -60,11 +61,13 @@ final class CreationPersonType extends AbstractType
->add('birthdate', ChillDateType::class, [ ->add('birthdate', ChillDateType::class, [
'required' => false, 'required' => false,
]) ])
->add('phonenumber', TelType::class, [ ->add('phonenumber', ChillPhoneNumberType::class, [
'required' => false, 'required' => false,
'type' => PhoneNumberType::FIXED_LINE,
]) ])
->add('mobilenumber', TelType::class, [ ->add('mobilenumber', ChillPhoneNumberType::class, [
'required' => false, 'required' => false,
'type' => PhoneNumberType::MOBILE,
]) ])
->add('email', EmailType::class, [ ->add('email', EmailType::class, [
'required' => false, 'required' => false,

View File

@ -14,26 +14,27 @@ namespace Chill\PersonBundle\Form;
use Chill\CustomFieldsBundle\Form\Type\CustomFieldType; use Chill\CustomFieldsBundle\Form\Type\CustomFieldType;
use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillDateType; use Chill\MainBundle\Form\Type\ChillDateType;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\CommentType; use Chill\MainBundle\Form\Type\CommentType;
use Chill\MainBundle\Form\Type\PickCivilityType; use Chill\MainBundle\Form\Type\PickCivilityType;
use Chill\MainBundle\Form\Type\Select2CountryType; use Chill\MainBundle\Form\Type\Select2CountryType;
use Chill\MainBundle\Form\Type\Select2LanguageType; use Chill\MainBundle\Form\Type\Select2LanguageType;
use Chill\MainBundle\Templating\TranslatableStringHelper; use Chill\MainBundle\Templating\TranslatableStringHelper;
use Chill\MainBundle\Templating\TranslatableStringHelperInterface;
use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper; use Chill\PersonBundle\Config\ConfigPersonAltNamesHelper;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Entity\PersonPhone; use Chill\PersonBundle\Entity\PersonPhone;
use Chill\PersonBundle\Form\Type\GenderType; use Chill\PersonBundle\Form\Type\GenderType;
use Chill\PersonBundle\Form\Type\PersonAltNameType; use Chill\PersonBundle\Form\Type\PersonAltNameType;
use Chill\PersonBundle\Form\Type\PersonPhoneType;
use Chill\PersonBundle\Form\Type\Select2MaritalStatusType; use Chill\PersonBundle\Form\Type\Select2MaritalStatusType;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\CallbackTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType; use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\DateType; use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\EmailType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\TelType;
use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
@ -57,17 +58,21 @@ class PersonType extends AbstractType
protected TranslatableStringHelper $translatableStringHelper; protected TranslatableStringHelper $translatableStringHelper;
private ParameterBagInterface $parameterBag;
/** /**
* @param string[] $personFieldsConfiguration configuration of visibility of some fields * @param string[] $personFieldsConfiguration configuration of visibility of some fields
*/ */
public function __construct( public function __construct(
array $personFieldsConfiguration, array $personFieldsConfiguration,
ConfigPersonAltNamesHelper $configAltNamesHelper, ConfigPersonAltNamesHelper $configAltNamesHelper,
TranslatableStringHelper $translatableStringHelper TranslatableStringHelperInterface $translatableStringHelper,
ParameterBagInterface $parameterBag
) { ) {
$this->config = $personFieldsConfiguration; $this->config = $personFieldsConfiguration;
$this->configAltNamesHelper = $configAltNamesHelper; $this->configAltNamesHelper = $configAltNamesHelper;
$this->translatableStringHelper = $translatableStringHelper; $this->translatableStringHelper = $translatableStringHelper;
$this->parameterBag = $parameterBag;
} }
public function buildForm(FormBuilderInterface $builder, array $options) public function buildForm(FormBuilderInterface $builder, array $options)
@ -126,22 +131,34 @@ class PersonType extends AbstractType
} }
if ('visible' === $this->config['phonenumber']) { if ('visible' === $this->config['phonenumber']) {
$builder->add('phonenumber', TelType::class, [ $builder
'required' => false, ->add(
// 'placeholder' => '+33623124554' //TODO placeholder for phone numbers 'phonenumber',
]); ChillPhoneNumberType::class,
[
'required' => false,
'type' => \libphonenumber\PhoneNumberType::FIXED_LINE,
]
);
} }
if ('visible' === $this->config['mobilenumber']) { if ('visible' === $this->config['mobilenumber']) {
$builder $builder
->add('mobilenumber', TelType::class, ['required' => false]) ->add(
'mobilenumber',
ChillPhoneNumberType::class,
[
'type' => \libphonenumber\PhoneNumberType::MOBILE,
'required' => false,
]
)
->add('acceptSMS', CheckboxType::class, [ ->add('acceptSMS', CheckboxType::class, [
'required' => false, 'required' => false,
]); ]);
} }
$builder->add('otherPhoneNumbers', ChillCollectionType::class, [ $builder->add('otherPhoneNumbers', ChillCollectionType::class, [
'entry_type' => PersonPhoneType::class, 'entry_type' => ChillPhoneNumberType::class,
'button_add_label' => 'Add new phone', 'button_add_label' => 'Add new phone',
'button_remove_label' => 'Remove phone', 'button_remove_label' => 'Remove phone',
'required' => false, 'required' => false,

View File

@ -146,22 +146,27 @@
'with_valid_from': false 'with_valid_from': false
}) }} }) }}
{% endif %} {% endif %}
<li> {% if person.phonenumber is not null %}
{% if person.mobilenumber %} <li>
<i class="fa fa-li fa-mobile"></i><a href="{{ 'tel:' ~ person.mobilenumber }}"> <i class="fa fa-li fa-phone"></i>
<a href="{{ 'tel:' ~ person.phonenumber|phone_number_format('E164') }}">
{{ person.phonenumber|chill_format_phonenumber }}
</a>
</li>
{% endif %}
{% if person.mobilenumber is not null %}
<li>
<i class="fa fa-li fa-mobile"></i><a href="{{ 'tel:' ~ person.mobilenumber|phone_number_format('E164') }}">
{{ person.mobilenumber|chill_format_phonenumber }} {{ person.mobilenumber|chill_format_phonenumber }}
</a> </a>
{% else %} </li>
{% endif %}
{% if person.phonenumber is null and person.mobilenumber is null %}
<li>
<i class="fa fa-li fa-phone"></i> <i class="fa fa-li fa-phone"></i>
{% if person.phonenumber %} <span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
<a href="{{ 'tel:' ~ person.phonenumber }}"> </li>
{{ person.phonenumber|chill_format_phonenumber }} {% endif %}
</a>
{% else %}
<span class="chill-no-data-statement">{{ 'No data given'|trans }}</span>
{% endif %}
{% endif %}
</li>
{% if options['addCenter'] and person|chill_resolve_center|length > 0 %} {% if options['addCenter'] and person|chill_resolve_center|length > 0 %}
<li> <li>
<i class="fa fa-li fa-long-arrow-right"></i> <i class="fa fa-li fa-long-arrow-right"></i>

View File

@ -25,14 +25,14 @@
{% if person.phonenumber %} {% if person.phonenumber %}
<span class="phonenumber d-block d-sm-inline-block"> <span class="phonenumber d-block d-sm-inline-block">
<i class="fa fa-fw fa-phone"></i> <i class="fa fa-fw fa-phone"></i>
<a href="{{ 'tel:' ~ person.phonenumber }}" class="phone mr-3" title="{{ 'Phonenumber'|trans }}"> <a href="{{ 'tel:' ~ person.phonenumber|phone_number_format('E164') }}" class="phone mr-3" title="{{ 'Phonenumber'|trans }}">
{{ person.phonenumber|chill_format_phonenumber }}</a> {{ person.phonenumber|chill_format_phonenumber }}</a>
</span> </span>
{% endif %} {% endif %}
{% if person.mobilenumber %} {% if person.mobilenumber %}
<span class="mobilenumber d-block d-sm-inline-block"> <span class="mobilenumber d-block d-sm-inline-block">
<i class="fa fa-fw fa-mobile"></i> <i class="fa fa-fw fa-mobile"></i>
<a href="{{ 'tel:' ~ person.mobilenumber }}" class="phone mr-3" title="{{ 'Mobilenumber'|trans }}"> <a href="{{ 'tel:' ~ person.mobilenumber|phone_number_format('E164') }}" class="phone mr-3" title="{{ 'Mobilenumber'|trans }}">
{{ person.mobilenumber|chill_format_phonenumber }}</a> {{ person.mobilenumber|chill_format_phonenumber }}</a>
</span> </span>
{% endif %} {% endif %}

View File

@ -62,12 +62,12 @@
<ul> <ul>
{% if person.phonenumber is not empty %} {% if person.phonenumber is not empty %}
<li> <li>
<a href="tel:{{ person.phonenumber }}"><img src="{{ asset('build/images/mobile-alt-solid.svg') }}">&nbsp;<pre>{{ person.phonenumber|chill_format_phonenumber }}</pre></a> <a href="tel:{{ person.phonenumber|phone_number_format('E164') }}"><img src="{{ asset('build/images/mobile-alt-solid.svg') }}">&nbsp;<pre>{{ person.phonenumber|chill_format_phonenumber }}</pre></a>
</li> </li>
{% endif %} {% endif %}
{% if person.mobilenumber is not empty%} {% if person.mobilenumber is not empty%}
<li> <li>
<a href="tel:{{ person.mobilenumber }}"><img src="{{ asset('build/images/phone-alt-solid.svg') }}">&nbsp;<pre>{{ person.mobilenumber|chill_format_phonenumber }}</pre></a> <a href="tel:{{ person.mobilenumber|phone_number_format('E164') }}"><img src="{{ asset('build/images/phone-alt-solid.svg') }}">&nbsp;<pre>{{ person.mobilenumber|chill_format_phonenumber }}</pre></a>
</li> </li>
{% endif %} {% endif %}
</ul> </ul>

View File

@ -232,14 +232,14 @@ This view should receive those arguments:
{%- if chill_person.fields.phonenumber == 'visible' -%} {%- if chill_person.fields.phonenumber == 'visible' -%}
<dl> <dl>
<dt>{{ 'Phonenumber'|trans }}&nbsp;:</dt> <dt>{{ 'Phonenumber'|trans }}&nbsp;:</dt>
<dd>{% if person.phonenumber is not empty %}<a href="tel:{{ person.phonenumber }}"><pre>{{ person.phonenumber|chill_format_phonenumber }}</pre></a>{% else %}<span class="chill-no-data-statement">{{ 'No data given'|trans }}{% endif %}</dd> <dd>{% if person.phonenumber is not empty %}<a href="tel:{{ person.phonenumber|phone_number_format('E164') }}">{{ person.phonenumber|chill_format_phonenumber }}</a>{% else %}<span class="chill-no-data-statement">{{ 'No data given'|trans }}{% endif %}</dd>
</dl> </dl>
{% endif %} {% endif %}
{%- if chill_person.fields.mobilenumber == 'visible' -%} {%- if chill_person.fields.mobilenumber == 'visible' -%}
<dl> <dl>
<dt>{{ 'Mobilenumber'|trans }}&nbsp;:</dt> <dt>{{ 'Mobilenumber'|trans }}&nbsp;:</dt>
<dd>{% if person.mobilenumber is not empty %}<a href="tel:{{ person.mobilenumber }}">{{ person.mobilenumber|chill_format_phonenumber }}</a>{% else %}<span class="chill-no-data-statement">{{ 'No data given'|trans }}{% endif %}</dd> <dd>{% if person.mobilenumber is not empty %}<a href="tel:{{ person.mobilenumber|phone_number_format('E164') }}">{{ person.mobilenumber|chill_format_phonenumber }}</a>{% else %}<span class="chill-no-data-statement">{{ 'No data given'|trans }}{% endif %}</dd>
<p>{% if person.acceptSMS %}{{ 'Accept short text message'|trans }}{% endif %}</p> <p>{% if person.acceptSMS %}{{ 'Accept short text message'|trans }}{% endif %}</p>
</dl> </dl>
{% endif %} {% endif %}
@ -250,7 +250,7 @@ This view should receive those arguments:
<dt>{{ 'Others phone numbers'|trans }}&nbsp;:</dt> <dt>{{ 'Others phone numbers'|trans }}&nbsp;:</dt>
{% for el in person.otherPhoneNumbers %} {% for el in person.otherPhoneNumbers %}
{% if el.phonenumber is not empty %} {% if el.phonenumber is not empty %}
<dd>{% if el.description is not empty %}{{ el.description }}&nbsp;:&nbsp;{% endif %}<a href="tel:{{ el.phonenumber }}">{{ el.phonenumber|chill_format_phonenumber }}</a></dd> <dd>{% if el.description is not empty %}{{ el.description }}&nbsp;:&nbsp;{% endif %}<a href="tel:{{ el.phonenumber|phone_number_format('E164') }}">{{ el.phonenumber|chill_format_phonenumber }}</a></dd>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul> </ul>
@ -317,4 +317,4 @@ This view should receive those arguments:
{% endif %} {% endif %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -95,9 +95,9 @@ class PersonDocGenNormalizer implements
'maritalStatus' => null !== ($ms = $person->getMaritalStatus()) ? $this->translatableStringHelper->localize($ms->getName()) : '', 'maritalStatus' => null !== ($ms = $person->getMaritalStatus()) ? $this->translatableStringHelper->localize($ms->getName()) : '',
'maritalStatusDate' => $this->normalizer->normalize($person->getMaritalStatusDate(), $format, $dateContext), 'maritalStatusDate' => $this->normalizer->normalize($person->getMaritalStatusDate(), $format, $dateContext),
'email' => $person->getEmail(), 'email' => $person->getEmail(),
'firstPhoneNumber' => $person->getPhonenumber() ?? $person->getMobilenumber(), 'firstPhoneNumber' => $this->normalizer->normalize($person->getPhonenumber() ?? $person->getMobilenumber(), $format, $context),
'fixPhoneNumber' => $person->getPhonenumber(), 'fixPhoneNumber' => $this->normalizer->normalize($person->getPhonenumber(), $format, $context),
'mobilePhoneNumber' => $person->getMobilenumber(), 'mobilePhoneNumber' => $this->normalizer->normalize($person->getMobilenumber(), $format, $context),
'nationality' => null !== ($c = $person->getNationality()) ? $this->translatableStringHelper->localize($c->getName()) : '', 'nationality' => null !== ($c = $person->getNationality()) ? $this->translatableStringHelper->localize($c->getName()) : '',
'placeOfBirth' => $person->getPlaceOfBirth(), 'placeOfBirth' => $person->getPlaceOfBirth(),
'memo' => $person->getMemo(), 'memo' => $person->getMemo(),

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\PersonBundle\Serializer\Normalizer; namespace Chill\PersonBundle\Serializer\Normalizer;
use Chill\MainBundle\Entity\Center; use Chill\MainBundle\Entity\Center;
use Chill\MainBundle\Phonenumber\PhoneNumberHelperInterface;
use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface; use Chill\MainBundle\Security\Resolver\CenterResolverManagerInterface;
use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension; use Chill\MainBundle\Templating\Entity\ChillEntityRenderExtension;
use Chill\PersonBundle\Entity\Person; use Chill\PersonBundle\Entity\Person;
@ -20,6 +21,7 @@ use Chill\PersonBundle\Repository\PersonRepository;
use DateTime; use DateTime;
use DateTimeImmutable; use DateTimeImmutable;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use libphonenumber\PhoneNumber;
use Symfony\Component\Serializer\Exception\UnexpectedValueException; use Symfony\Component\Serializer\Exception\UnexpectedValueException;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareInterface;
use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait; use Symfony\Component\Serializer\Normalizer\DenormalizerAwareTrait;
@ -41,6 +43,8 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
private CenterResolverManagerInterface $centerResolverManager; private CenterResolverManagerInterface $centerResolverManager;
private PhoneNumberHelperInterface $phoneNumberHelper;
private ChillEntityRenderExtension $render; private ChillEntityRenderExtension $render;
private PersonRepository $repository; private PersonRepository $repository;
@ -48,11 +52,13 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
public function __construct( public function __construct(
ChillEntityRenderExtension $render, ChillEntityRenderExtension $render,
PersonRepository $repository, PersonRepository $repository,
CenterResolverManagerInterface $centerResolverManager CenterResolverManagerInterface $centerResolverManager,
PhoneNumberHelperInterface $phoneNumberHelper
) { ) {
$this->render = $render; $this->render = $render;
$this->repository = $repository; $this->repository = $repository;
$this->centerResolverManager = $centerResolverManager; $this->centerResolverManager = $centerResolverManager;
$this->phoneNumberHelper = $phoneNumberHelper;
} }
public function denormalize($data, $type, $format = null, array $context = []) public function denormalize($data, $type, $format = null, array $context = [])
@ -106,12 +112,12 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
break; break;
case 'phonenumber': case 'phonenumber':
$person->setPhonenumber($data[$item]); $person->setPhonenumber($this->denormalizer->denormalize($data[$item], PhoneNumber::class, $format, $context));
break; break;
case 'mobilenumber': case 'mobilenumber':
$person->setMobilenumber($data[$item]); $person->setMobilenumber($this->denormalizer->denormalize($data[$item], PhoneNumber::class, $format, $context));
break; break;
@ -187,8 +193,8 @@ class PersonJsonNormalizer implements DenormalizerAwareInterface, NormalizerAwar
'deathdate' => $this->normalizer->normalize($person->getDeathdate(), $format, $context), 'deathdate' => $this->normalizer->normalize($person->getDeathdate(), $format, $context),
'age' => $this->normalizer->normalize($person->getAge(), $format, $context), 'age' => $this->normalizer->normalize($person->getAge(), $format, $context),
'centers' => $this->normalizer->normalize($this->centerResolverManager->resolveCenters($person), $format, $context), 'centers' => $this->normalizer->normalize($this->centerResolverManager->resolveCenters($person), $format, $context),
'phonenumber' => $person->getPhonenumber(), 'phonenumber' => $this->normalizer->normalize($person->getPhonenumber()),
'mobilenumber' => $person->getMobilenumber(), 'mobilenumber' => $this->normalizer->normalize($person->getMobilenumber()),
'email' => $person->getEmail(), 'email' => $person->getEmail(),
'altNames' => $this->normalizeAltNames($person->getAltNames()), 'altNames' => $this->normalizeAltNames($person->getAltNames()),
'gender' => $person->getGender(), 'gender' => $person->getGender(),

View File

@ -297,13 +297,14 @@ final class PersonControllerUpdateTest extends WebTestCase
// reminder: this value is capitalized // reminder: this value is capitalized
['placeOfBirth', 'A PLACE', static function (Person $person) { return $person->getPlaceOfBirth(); }], ['placeOfBirth', 'A PLACE', static function (Person $person) { return $person->getPlaceOfBirth(); }],
['birthdate', '1980-12-15', static function (Person $person) { return $person->getBirthdate()->format('Y-m-d'); }], ['birthdate', '1980-12-15', static function (Person $person) { return $person->getBirthdate()->format('Y-m-d'); }],
['phonenumber', '+32123456789', static function (Person $person) { return $person->getPhonenumber(); }], // TODO test on phonenumber update
// ['phonenumber', '+32123456789', static function (Person $person) { return $person->getPhonenumber(); }],
['memo', 'jfkdlmq jkfldmsq jkmfdsq', static function (Person $person) { return $person->getMemo(); }], ['memo', 'jfkdlmq jkfldmsq jkmfdsq', static function (Person $person) { return $person->getMemo(); }],
['countryOfBirth', 'BE', static function (Person $person) { return $person->getCountryOfBirth()->getCountryCode(); }], ['countryOfBirth', 'BE', static function (Person $person) { return $person->getCountryOfBirth()->getCountryCode(); }],
['nationality', 'FR', static function (Person $person) { return $person->getNationality()->getCountryCode(); }], ['nationality', 'FR', static function (Person $person) { return $person->getNationality()->getCountryCode(); }],
['placeOfBirth', '', static function (Person $person) { return $person->getPlaceOfBirth(); }], ['placeOfBirth', '', static function (Person $person) { return $person->getPlaceOfBirth(); }],
['birthdate', '', static function (Person $person) { return $person->getBirthdate(); }], ['birthdate', '', static function (Person $person) { return $person->getBirthdate(); }],
['phonenumber', '', static function (Person $person) { return $person->getPhonenumber(); }], //['phonenumber', '', static function (Person $person) { return $person->getPhonenumber(); }],
['memo', '', static function (Person $person) { return $person->getMemo(); }], ['memo', '', static function (Person $person) { return $person->getMemo(); }],
['countryOfBirth', null, static function (Person $person) { return $person->getCountryOfBirth(); }], ['countryOfBirth', null, static function (Person $person) { return $person->getCountryOfBirth(); }],
['nationality', null, static function (Person $person) { return $person->getNationality(); }], ['nationality', null, static function (Person $person) { return $person->getNationality(); }],

View File

@ -158,7 +158,7 @@ final class AccompanyingPeriodTest extends \PHPUnit\Framework\TestCase
$participationL = $period->closeParticipationFor($person); $participationL = $period->closeParticipationFor($person);
$this->assertSame($participationL, $participation); $this->assertSame($participationL, $participation);
$this->assertTrue($participation->getEndDate() instanceof DateTimeInterface); $this->assertTrue($participationL->getEndDate() instanceof DateTimeInterface);
$participation = $period->getOpenParticipationContainsPerson($person); $participation = $period->getOpenParticipationContainsPerson($person);
$this->assertNull($participation); $this->assertNull($participation);

View File

@ -1,15 +1,15 @@
services: services:
Chill\PersonBundle\Form\: Chill\PersonBundle\Form\:
autowire: true autowire: true
autoconfigure: true autoconfigure: true
resource: '../../Form/' resource: '../../Form/'
Chill\PersonBundle\Form\PersonType: Chill\PersonBundle\Form\PersonType:
autowire: true
autoconfigure: true
arguments: arguments:
$personFieldsConfiguration: '%chill_person.person_fields%' $personFieldsConfiguration: '%chill_person.person_fields%'
$configAltNamesHelper: '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper' $configAltNamesHelper: '@Chill\PersonBundle\Config\ConfigPersonAltNamesHelper'
$translatableStringHelper: '@Chill\MainBundle\Templating\TranslatableStringHelper'
tags: tags:
- { name: form.type, alias: '@chill.person.form.person_creation' } - { name: form.type, alias: '@chill.person.form.person_creation' }

View File

@ -19,8 +19,5 @@ Chill\PersonBundle\Entity\AccompanyingPeriod:
Chill\PersonBundle\Entity\PersonPhone: Chill\PersonBundle\Entity\PersonPhone:
properties: properties:
phonenumber: phonenumber:
- Regex:
pattern: '/^([\+{1}])([0-9\s*]{4,20})$/'
message: 'Invalid phone number: it should begin with the international prefix starting with "+", hold only digits and be smaller than 20 characters. Ex: +33123456789'
- Chill\MainBundle\Validation\Constraint\PhonenumberConstraint: - Chill\MainBundle\Validation\Constraint\PhonenumberConstraint:
type: any type: any

View File

@ -0,0 +1,86 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\Person;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use Exception;
use libphonenumber\PhoneNumberUtil;
use RuntimeException;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
final class Version20220215135509 extends AbstractMigration implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function down(Schema $schema): void
{
throw new Exception('You should not do that.');
}
public function getDescription(): string
{
return 'Update phone numbers for person';
}
public function up(Schema $schema): void
{
$carrier_code = $this->container
->getParameter('chill_main')['phone_helper']['default_carrier_code'];
if (null === $carrier_code) {
throw new RuntimeException('no carrier code');
}
$this->addSql('ALTER TABLE chill_person_person ALTER phonenumber TYPE TEXT');
$this->addSql('ALTER TABLE chill_person_person ALTER phonenumber DROP DEFAULT');
$this->addSql('ALTER TABLE chill_person_person ALTER phonenumber DROP NOT NULL');
$this->addSql('COMMENT ON COLUMN chill_person_person.phonenumber IS NULL');
$this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber TYPE TEXT');
$this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber DROP DEFAULT');
$this->addSql('ALTER TABLE chill_person_person ALTER mobilenumber DROP NOT NULL');
$this->addSql('COMMENT ON COLUMN chill_person_person.mobilenumber IS NULL');
$this->addSql(
'UPDATE chill_person_person SET ' .
$this->buildMigrationPhonenumberClause($carrier_code, 'phonenumber') .
', ' .
$this->buildMigrationPhoneNumberClause($carrier_code, 'mobilenumber')
);
$this->addSql('ALTER TABLE chill_person_phone ALTER phonenumber TYPE TEXT');
$this->addSql('ALTER TABLE chill_person_phone ALTER phonenumber DROP DEFAULT');
$this->addSql('ALTER TABLE chill_person_phone ALTER phonenumber DROP NOT NULL');
$this->addSql('COMMENT ON COLUMN chill_person_phone.phonenumber IS NULL');
$this->addSql(
'UPDATE chill_person_phone SET ' .
$this->buildMigrationPhoneNumberClause($carrier_code, 'phonenumber')
);
}
private function buildMigrationPhoneNumberClause(string $defaultCarriercode, string $field): string
{
$util = PhoneNumberUtil::getInstance();
$countryCode = $util->getCountryCodeForRegion($defaultCarriercode);
return sprintf('%s=CASE
WHEN %s = \'\' THEN NULL
WHEN LEFT(%s, 1) = \'0\'
THEN \'+%s\' || replace(replace(substr(%s, 2), \'(0)\', \'\'), \' \', \'\')
ELSE replace(replace(%s, \'(0)\', \'\'),\' \', \'\')
END', $field, $field, $field, $countryCode, $field, $field);
}
}

View File

@ -21,6 +21,7 @@ use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\Common\DataFixtures\DependentFixtureInterface; use Doctrine\Common\DataFixtures\DependentFixtureInterface;
use Doctrine\Persistence\ObjectManager; use Doctrine\Persistence\ObjectManager;
use Iterator; use Iterator;
use libphonenumber\PhoneNumberUtil;
use Nelmio\Alice\Loader\NativeLoader; use Nelmio\Alice\Loader\NativeLoader;
use Nelmio\Alice\ObjectSet; use Nelmio\Alice\ObjectSet;
@ -29,6 +30,13 @@ use function count;
class LoadThirdParty extends Fixture implements DependentFixtureInterface class LoadThirdParty extends Fixture implements DependentFixtureInterface
{ {
private PhoneNumberUtil $phoneNumberUtil;
public function __construct()
{
$this->phoneNumberUtil = PhoneNumberUtil::getInstance();
}
public function getDependencies() public function getDependencies()
{ {
return [ return [
@ -66,7 +74,7 @@ class LoadThirdParty extends Fixture implements DependentFixtureInterface
Address::class => [ Address::class => [
'address1' => [ 'address1' => [
'name' => '<fr_FR:company()>', 'name' => '<fr_FR:company()>',
'telephone' => '<fr_FR:phonenumber()>', 'telephone' => $this->phoneNumberUtil->getExampleNumber('FR'),
'email' => '<email()>', 'email' => '<email()>',
'comment' => '<fr_FR:realTextBetween(10, 500)>', 'comment' => '<fr_FR:realTextBetween(10, 500)>',
], ],
@ -116,7 +124,7 @@ class LoadThirdParty extends Fixture implements DependentFixtureInterface
ThirdParty::class => [ ThirdParty::class => [
'thirdparty{1..75}' => [ 'thirdparty{1..75}' => [
'name' => '<fr_FR:company()>', 'name' => '<fr_FR:company()>',
'telephone' => '<fr_FR:phonenumber()>', 'telephone' => $this->phoneNumberUtil->getExampleNumber('FR'),
'email' => '<email()>', 'email' => '<email()>',
'comment' => '<fr_FR:realTextBetween(10, 500)>', 'comment' => '<fr_FR:realTextBetween(10, 500)>',
'address' => '@address<current()>', 'address' => '@address<current()>',

View File

@ -24,6 +24,7 @@ use DateTimeInterface;
use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\Mapping as ORM;
use libphonenumber\PhoneNumber;
use Symfony\Component\Serializer\Annotation\Context; use Symfony\Component\Serializer\Annotation\Context;
use Symfony\Component\Serializer\Annotation\DiscriminatorMap; use Symfony\Component\Serializer\Annotation\DiscriminatorMap;
use Symfony\Component\Serializer\Annotation\Groups; use Symfony\Component\Serializer\Annotation\Groups;
@ -253,14 +254,11 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
private ?ThirdPartyProfession $profession = null; private ?ThirdPartyProfession $profession = null;
/** /**
* @ORM\Column(name="telephone", type="string", length=64, nullable=true) * @ORM\Column(name="telephone", type="phone_number", nullable=true)
* @Assert\Regex("/^([\+{1}])([0-9\s*]{4,20})$/",
* message="Invalid phone number: it should begin with the international prefix starting with ""+"", hold only digits and be smaller than 20 characters. Ex: +33123456789"
* )
* @PhonenumberConstraint(type="any") * @PhonenumberConstraint(type="any")
* @Groups({"read", "write", "docgen:read", "docgen:read:3party:parent"}) * @Groups({"read", "write", "docgen:read", "docgen:read:3party:parent"})
*/ */
private ?string $telephone = null; private ?PhoneNumber $telephone = null;
/** /**
* @ORM\Column(name="types", type="json", nullable=true) * @ORM\Column(name="types", type="json", nullable=true)
@ -502,7 +500,7 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
/** /**
* Get telephone. * Get telephone.
*/ */
public function getTelephone(): ?string public function getTelephone(): ?PhoneNumber
{ {
return $this->telephone; return $this->telephone;
} }
@ -821,12 +819,8 @@ class ThirdParty implements TrackCreationInterface, TrackUpdateInterface
/** /**
* Set telephone. * Set telephone.
*
* @param string|null $telephone
*
* @return ThirdParty
*/ */
public function setTelephone($telephone = null) public function setTelephone(?PhoneNumber $telephone = null): self
{ {
$this->telephone = $telephone; $this->telephone = $telephone;

View File

@ -12,6 +12,7 @@ declare(strict_types=1);
namespace Chill\ThirdPartyBundle\Form; namespace Chill\ThirdPartyBundle\Form;
use Chill\MainBundle\Form\Type\ChillCollectionType; use Chill\MainBundle\Form\Type\ChillCollectionType;
use Chill\MainBundle\Form\Type\ChillPhoneNumberType;
use Chill\MainBundle\Form\Type\ChillTextareaType; use Chill\MainBundle\Form\Type\ChillTextareaType;
use Chill\MainBundle\Form\Type\PickAddressType; use Chill\MainBundle\Form\Type\PickAddressType;
use Chill\MainBundle\Form\Type\PickCenterType; use Chill\MainBundle\Form\Type\PickCenterType;
@ -75,7 +76,7 @@ class ThirdPartyType extends AbstractType
->add('name', TextType::class, [ ->add('name', TextType::class, [
'required' => true, 'required' => true,
]) ])
->add('telephone', TextType::class, [ ->add('telephone', ChillPhoneNumberType::class, [
'label' => 'Phonenumber', 'label' => 'Phonenumber',
'required' => false, 'required' => false,
]) ])

View File

@ -110,8 +110,8 @@
}) }} }) }}
</li> </li>
<li><i class="fa fa-li fa-phone"></i> <li><i class="fa fa-li fa-phone"></i>
{% if thirdparty.telephone %} {% if thirdparty.telephone is not null %}
<a href="{{ 'tel:' ~ thirdparty.telephone }}">{{ thirdparty.telephone|chill_format_phonenumber }}</a> <a href="{{ 'tel:' ~ thirdparty.telephone|phone_number_format('E164') }}">{{ thirdparty.telephone|chill_format_phonenumber }}</a>
{% else %} {% else %}
<span class="chill-no-data-statement">{{ 'thirdparty.No_phonenumber'|trans }}</span> <span class="chill-no-data-statement">{{ 'thirdparty.No_phonenumber'|trans }}</span>
{% endif %} {% endif %}
@ -136,7 +136,7 @@
</li> </li>
<li><i class="fa fa-li fa-phone"></i> <li><i class="fa fa-li fa-phone"></i>
{% if thirdparty.telephone %} {% if thirdparty.telephone %}
<a href="{{ 'tel:' ~ thirdparty.telephone }}">{{ thirdparty.telephone|chill_format_phonenumber }}</a> <a href="{{ 'tel:' ~ thirdparty.telephone|phone_number_format('E164') }}">{{ thirdparty.telephone|chill_format_phonenumber }}</a>
{% else %} {% else %}
<span class="chill-no-data-statement">{{ 'thirdparty.No_phonenumber'|trans }}</span> <span class="chill-no-data-statement">{{ 'thirdparty.No_phonenumber'|trans }}</span>
{% endif %} {% endif %}

View File

@ -67,10 +67,10 @@
<dt>{{ 'Phonenumber'|trans }}</dt> <dt>{{ 'Phonenumber'|trans }}</dt>
<dd> <dd>
{% if thirdParty.telephone == null %} {% if thirdParty.telephone == null %}
<span class="chill-no-data-statement">{{ 'No phone given'|trans }}</span> <span class="chill-no-data-statement">{{ 'thirdparty.No_phonenumber'|trans }}</span>
{% else %} {% else %}
<a href="{{ 'tel:' ~ thirdParty.telephone }}"> <a href="{{ 'tel:' ~ thirdParty.telephone|phone_number_format('E164') }}">
{{ thirdParty.telephone|chill_print_or_message("thirdparty.No_phonenumber") }} {{ thirdParty.telephone|chill_format_phonenumber }}
</a> </a>
{% endif %} {% endif %}
</dd> </dd>

View File

@ -62,7 +62,7 @@ class ThirdPartyNormalizer implements NormalizerAwareInterface, NormalizerInterf
}, $thirdParty->getTypesAndCategories()), }, $thirdParty->getTypesAndCategories()),
'profession' => $this->normalizer->normalize($thirdParty->getProfession(), $format, $context), 'profession' => $this->normalizer->normalize($thirdParty->getProfession(), $format, $context),
'address' => $this->normalizer->normalize($thirdParty->getAddress(), $format, ['address_rendering' => 'short']), 'address' => $this->normalizer->normalize($thirdParty->getAddress(), $format, ['address_rendering' => 'short']),
'phonenumber' => $thirdParty->getTelephone(), 'phonenumber' => $this->normalizer->normalize($thirdParty->getTelephone()),
'email' => $thirdParty->getEmail(), 'email' => $thirdParty->getEmail(),
'isChild' => $thirdParty->isChild(), 'isChild' => $thirdParty->isChild(),
'parent' => $this->normalizer->normalize($thirdParty->getParent(), $format, $context), 'parent' => $this->normalizer->normalize($thirdParty->getParent(), $format, $context),

View File

@ -0,0 +1,70 @@
<?php
/**
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
declare(strict_types=1);
namespace Chill\Migrations\ThirdParty;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
use libphonenumber\PhoneNumberUtil;
use RuntimeException;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerAwareTrait;
final class Version20220302143821 extends AbstractMigration implements ContainerAwareInterface
{
use ContainerAwareTrait;
public function down(Schema $schema): void
{
$this->addSql('ALTER TABLE chill_3party.third_party ALTER telephone TYPE VARCHAR(64)');
$this->addSql('ALTER TABLE chill_3party.third_party ALTER telephone DROP DEFAULT');
$this->addSql('COMMENT ON COLUMN chill_3party.third_party.telephone IS NULL');
}
public function getDescription(): string
{
return 'Upgrade phonenumber on third parties';
}
public function up(Schema $schema): void
{
$carrier_code = $this->container
->getParameter('chill_main')['phone_helper']['default_carrier_code'];
if (null === $carrier_code) {
throw new RuntimeException('no carrier code');
}
$this->addSql('ALTER TABLE chill_3party.third_party ALTER telephone TYPE VARCHAR(35)');
$this->addSql('ALTER TABLE chill_3party.third_party ALTER telephone DROP DEFAULT');
$this->addSql('ALTER TABLE chill_3party.third_party ALTER telephone TYPE VARCHAR(35)');
$this->addSql('COMMENT ON COLUMN chill_3party.third_party.telephone IS \'(DC2Type:phone_number)\'');
$this->addSql(
'UPDATE chill_3party.third_party SET ' .
$this->buildMigrationPhonenumberClause($carrier_code, 'telephone')
);
}
private function buildMigrationPhoneNumberClause(string $defaultCarriercode, string $field): string
{
$util = PhoneNumberUtil::getInstance();
$countryCode = $util->getCountryCodeForRegion($defaultCarriercode);
return sprintf('%s=CASE
WHEN %s = \'\' THEN NULL
WHEN LEFT(%s, 1) = \'0\'
THEN \'+%s\' || replace(replace(substr(%s, 2), \'(0)\', \'\'), \' \', \'\')
ELSE replace(replace(%s, \'(0)\', \'\'),\' \', \'\')
END', $field, $field, $field, $countryCode, $field, $field);
}
}

@ -1 +1 @@
Subproject commit 0fef0f21602989ed3aa6b301080ae406d71dd632 Subproject commit 3961348aa322b98fff625c09d79f8d2f3cd4d6ae