Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- <?php
- declare(strict_types=1);
- namespace App\Shell\Task;
- use App\Command\Helper\DataMapHelper;
- use App\Command\Helper\DataMapHelper\DataMapInterface;
- use App\Model\Entity\Contact;
- use App\Model\Entity\User;
- use Cake\Collection\Collection;
- use Cake\Console\ConsoleIo;
- use Cake\Core\Exception\Exception;
- use Cake\Log\Log;
- use Cake\ORM\Locator\LocatorAwareTrait;
- use Cake\ORM\Locator\LocatorInterface;
- use InvalidArgumentException;
- use Queue\Model\QueueException;
- use Queue\Shell\Task\AddInterface;
- use Queue\Shell\Task\QueueTask;
- use SplFileObject;
- use Throwable;
- /**
- * Task to make sure that contacts can be added to the database without timing out the server.
- *
- * Especially useful for imports that have a large number of items.
- * Must include a CSV file and user identifier.
- *
- * @package App\Shell\Task
- * @property \App\Model\Table\UsersTable $Users Used to associate contacts with the current user.
- * @property \App\Model\Table\ContactsTable $Contacts Used to process contact data.
- * @property \App\Model\Table\CompaniesTable $Companies Used to associate company data with contacts.
- * @property \App\Command\Helper\DataMapHelper $DataMap Used to create and work with data maps.
- */
- class QueueAddContactsTask extends QueueTask implements addInterface
- {
- use LocatorAwareTrait;
- /**
- * Timeout in seconds, after which the Task is reassigned to a new worker
- * if not finished successfully.
- * This should be high enough that it cannot still be running on a zombie worker (>> 2x).
- * Defaults to Config::defaultworkertimeout().
- * Default maximum execution time on most servers is one minute.
- *
- * @var float|int
- */
- public $timeout = 60 * 5;
- /**
- * All company names to check for duplicates.
- *
- * @var array
- */
- public array $cNames;
- /**
- * Number of new contacts that have been added.
- *
- * @var int
- */
- public int $newCount;
- /**
- * Number of contacts that have been updated.
- *
- * @var int
- */
- public int $updateCount;
- /**
- * Number of contacts to be added or updated.
- *
- * @var int
- */
- public int $totalRecords;
- public array $contactEntities;
- public array $lists;
- public int $user_id;
- public string $filePath;
- public DataMapInterface $map;
- public DataMapHelper $DataMap;
- /**
- * @param \Cake\Console\ConsoleIo|null $io
- * @param \Cake\ORM\Locator\LocatorInterface|null $locator
- */
- public function __construct(?ConsoleIo $io = null, ?LocatorInterface $locator = null)
- {
- parent::__construct($io, $locator);
- $this->Users = $this->loadModel('Users');
- $this->Contacts = $this->loadModel('Contacts');
- $this->Companies = $this->loadModel('Companies');
- $this->cNames = [];
- $this->contactEntities = [];
- $this->lists = [];
- $this->newCount = 0;
- $this->updateCount = 0;
- $this->totalRecords = 0;
- $this->DataMap = $this->helper('DataMap');
- }
- /**
- * @param array $data Payload
- * @param int $jobId The ID of the QueuedJob entity
- * @return void
- * @throws \Throwable
- */
- public function run(array $data, int $jobId): void
- {
- $this->newCount = 0;
- $this->updateCount = 0;
- $this->totalRecords = 0;
- try {
- // Initializing
- $jobsTable = $this->getTableLocator()->get('Queue.QueuedJobs');
- // Part One
- $jobsTable->updateAll(
- ['status' => 'Checking Data'],
- ['id' => $jobId]
- );
- $this->out('Checking Data');
- $this->filePath = $data['filePath'];
- if (!isset($data['filePath'])) {
- throw new QueueException('AddContacts Task called without file path.');
- }
- if (!isset($data['identifier'])) {
- throw new QueueException('AddContacts Task called without user identifier.');
- }
- $this->user_id = $data['identifier'];
- if (!isset($data['listIDs'])) {
- throw new QueueException('AddContacts Task called without associated contact lists.');
- }
- try {
- $fileObj = new SplFileObject($data['filePath']);
- } catch (Throwable $e) {
- $error = $e->getMessage();
- $error .= ' (line ' . $e->getLine() . ' in ' . $e->getFile() . ')' . PHP_EOL . $e->getTraceAsString();
- Log::write('error', $error);
- $this->QueuedJobs->updateProgress($jobId, 100, 'Error: Contacts not imported. Contact support if this issue persists.');
- throw $e;
- }
- if (is_object($fileObj) && $fileObj instanceof SplFileObject) {
- try {
- if (!$this->isCSV($fileObj)) {
- throw new QueueException('AddContacts Task called with non csv data.');
- }
- if (!empty($data["listIDs"])) {
- $this->lists = $data['listIDs'];
- }
- $this->map = $this->getDataMap($data['mapData']);
- $importArray = $this->toArray($fileObj, $this->map, true);
- $importArray = $this->removeDuplicates($importArray);
- $this->totalRecords = count($importArray);
- if ($this->totalRecords === 0) {
- throw new QueueException('AddContacts Task with an empty csv file');
- }
- $jobsTable->updateProgress($jobId, 50); // Passed data verification.
- // Part Two
- $message = __("Saving Data", $this->totalRecords);
- $jobsTable->updateAll(['status' => $message], ['id' => $jobId]);
- $this->out($message);
- $user = $this->Users->get($data['identifier']);
- $existingContacts = $this->getExistingContacts($importArray, $this->user_id);
- foreach ($existingContacts as $key => $existingContact) {
- $existingContacts[$key] = $this->associateCompany($existingContact, $user);
- }
- if (!$this->saveAllExistingContacts($existingContacts, $this->user_id, $data)) {
- $this->err("Could not save existing contacts");
- }
- $newContacts = $this->getNewContacts($importArray, $this->user_id);
- foreach ($newContacts as $key => $newContact) {
- $newContacts[$key] = $this->associateCompany($newContact, $user);
- }
- if (!$this->saveAllNewContacts($newContacts, $this->user_id, $data)) {
- $this->err("Could not save new contacts");
- }
- $message = "All contacts successfully imported!";
- $jobsTable->updateProgress($jobId, 100);
- $jobsTable->updateAll(['status' => $message], ['id' => $jobId]);
- $this->success($message);
- } catch (Throwable $e) {
- $error = $e->getMessage();
- $error .= ' (line ' . $e->getLine() . ' in ' . $e->getFile() . ')' . PHP_EOL . $e->getTraceAsString();
- Log::write('error', $error);
- $this->QueuedJobs->updateProgress($jobId, 100, 'Error: Contacts not imported. Contact support if this issue persists.');
- throw $e;
- }
- } else {
- $this->QueuedJobs->updateProgress($jobId, 100, 'Error: Contacts not imported. Please use a valid CSV.');
- throw new QueueException('AddContacts Task called with invalid file data.');
- }
- } catch (throwable $e) {
- $this->err('Could not finish import. Error thrown in ' . $e->getFile() . ' on line ' . $e->getLine() . '.');
- throw $e;
- }
- }
- /**
- * Verify if file is a valid CSV.
- *
- * @param \SplFileObject $fileObj File to be checked.
- * @return bool Result of check as a true or false value.
- */
- protected function isCSV(SplFileObject $fileObj): bool
- {
- return $fileObj->isReadable();
- }
- /**
- * Get Data Map Object.
- * @param string $data
- * @return \App\Command\Helper\DataMapHelper\DataMapInterface
- */
- public function getDataMap(string $data): DataMapInterface
- {
- return $this->DataMap->getDataMap($data);
- }
- /**
- * Convert CSV to an array
- *
- * @param \SplFileObject $fileObj
- * @param \App\Command\Helper\DataMapHelper\DataMapInterface $map
- * @param bool $hasHeaders
- * @return array|null
- */
- public function toArray(SplFileObject $fileObj, DataMapInterface $map, bool $hasHeaders = false): ?array
- {
- $fileObj->setFlags(SplFileObject::READ_CSV);
- $data = new Collection($fileObj);
- if ($hasHeaders) {
- $headers = $this->getHeaders($fileObj);
- $rows = $data->skip(1);
- $array = [];
- foreach ($rows->toArray() as $row) {
- if (count($row) == count($headers)) {
- $array[] = $row;
- }
- }
- $result = array_map(function ($x) use ($headers) {
- return array_combine($headers, $x);
- }, $array);
- $result = $this->mapContacts($result, $map);
- } else {
- $rows = $data->toArray();
- foreach ($rows as $row) {
- if (count($row) > 1) {
- $result[] = $row;
- }
- }
- }
- return $result ?? null;
- }
- /**
- * Get all headers into the array and ready to be imported.
- *
- * @param \SplFileObject $fileObj
- * @return array
- */
- protected function getHeaders(SplFileObject $fileObj): array
- {
- $fileObj->setFlags(SplFileObject::READ_CSV);
- $data = new Collection($fileObj);
- $raw = $data->first();
- $result = [];
- foreach ($raw as $item) {
- $result[] = $item;
- }
- return $result;
- }
- public function mapContacts(array $rawArray, DataMapInterface $map)
- {
- $result = [];
- foreach ($rawArray as $key => $contact) {
- $this->replaceValue('first_name', $map->first_name(), $contact);
- $this->replaceValue('last_name', $map->last_name(), $contact);
- $this->replaceValue('job_title', $map->job_title(), $contact);
- $this->replaceValue('phone', $map->phone(), $contact);
- $this->replaceValue('can_text', $map->can_text(), $contact);
- $this->replaceValue('email', $map->email(), $contact);
- $this->replaceValue('street_address', $map->street_address(), $contact);
- $this->replaceValue('city', $map->city(), $contact);
- $this->replaceValue('state', $map->state(), $contact);
- $this->replaceValue('postal_code', $map->postal_code(), $contact);
- $this->replaceValue('country', $map->country(), $contact);
- $this->replaceValue('notes', $map->notes(), $contact);
- $this->replaceValue('Company.name', $map->company_name(), $contact);
- $result[$key] = $contact;
- }
- return $result;
- }
- /**
- * Replace current key value pair with a new one that has the same value but an altered key.
- *
- * @param string $newKey
- * @param string $oldKey
- * @param array $array
- */
- public function replaceValue(string $newKey, string $oldKey, array &$array): void
- {
- if (empty($array)) {
- throw new Exception("Array is empty.");
- }
- if ($oldKey !== $newKey) {
- if (isset($array[$oldKey])) {
- $array[$newKey] = $array[$oldKey];
- unset($array[$oldKey]);
- } else {
- $array[$newKey] = null;
- }
- }
- }
- /**
- * Get all unique values in an array.
- *
- * @param array $assocArray Array to be cleaned.
- * @return array Cleaned associative array.
- */
- public function removeDuplicates(array $assocArray): array
- {
- $map = array_map("serialize", $assocArray);
- $scrubbed = array_unique($map);
- return array_map("unserialize", $scrubbed);
- }
- /**
- * Get all existing contacts from submission.
- *
- * @param array $submissions All submissions to be evaluated and sorted.
- * @param int $user_id ID of the user who submitted the contact data.
- * @return array All existing contacts in submissions without any duplications.
- */
- public function getExistingContacts(array $submissions, int $user_id): array
- {
- $results = [];
- $submissions = $this->removeDuplicates($submissions);
- foreach ($submissions as $submission) {
- if ($this->submissionExists($submission, $user_id)) {
- $results[] = $submission;
- }
- }
- return $results;
- }
- /**
- * Check to see if contact is a duplicate of submitted list of contacts.
- *
- * @param array $submission Contact data to be evaluated
- * @param int $user_id ID of the user who submitted the contact data.
- * @return bool Result of the check, if true then the contact already exists in the database.
- */
- public function submissionExists(array $submission, int $user_id): bool
- {
- $contacts = $this->Contacts->find()->where(["user_id" => $user_id]);
- if ($contacts->count() > 0) {
- foreach ($contacts as $contact) {
- if (
- $submission['first_name'] === $contact->first_name &&
- $submission['last_name'] === $contact->last_name &&
- $submission['email'] === $contact->email
- ) {
- return true;
- }
- }
- }
- return false;
- }
- /**
- * Associate a user with the contact and generate updated version of the row.
- *
- * @param array $row Row data to be modified.
- * @param \App\Model\Entity\User $user User with which to associate data.
- * @return array Altered row.
- */
- public function associateCompany(array $row, User $user): array
- {
- $row['user_id'] = $user->id;
- if (isset($row['Company.name'])) {
- $company = $this->Companies->find()->where([
- 'name' => $row['Company.name'],
- 'user_id' => $user->id,
- ]);
- if (!$company->isEmpty()) {
- if (!in_array($row['Company.name'], $this->cNames)) {
- $companyEntityArray = [
- 'user_id' => $user->id,
- 'name' => $row['Company.name'],
- ];
- $row['company'] = $companyEntityArray;
- $this->cNames[] = $row['Company.name'];
- } else {
- $repeat = $company->first();
- $row['company_id'] = $repeat->id ?? '';
- }
- } else {
- $company = $this->createNewCompany($row['Company.name'], $user->id);
- if ($company) {
- $row['company_id'] = $company->id;
- } else {
- throw new Exception("Could not create new company");
- }
- }
- }
- return $row;
- }
- /**
- * @param array $submissions
- * @param int $user_id
- * @param $data
- * @return bool
- * @throws \Exception'
- */
- public function saveAllExistingContacts(array $submissions, int $user_id, $data): bool
- {
- $existingContacts = $this->getExistingContacts($submissions, $user_id);
- if ($existingContacts) {
- $contacts = [];
- foreach ($existingContacts as $contact) {
- $match = $this->findExisting($contact, $user_id);
- if ($match) {
- $new = $this->Contacts->patchEntity($match, $contact);
- if (!$new->can_text) {
- $new->can_text = false;
- }
- if (!$new->notes) {
- $new->notes = null;
- }
- $contacts = $this->addToContactArray($new, $contacts);
- }
- }
- if ($contacts) {
- if ($this->Contacts->saveManyOrFail($contacts, ['associated' => ['Companies']])) {
- foreach ($data['listIDs'] as $list) {
- $needLinked = [];
- /** @var \App\Model\Entity\Contact $contact */
- foreach ($contacts as $contact) {
- $contactIsInList = $this->isContactInList($contact->id, $list);
- if (!$contactIsInList) {
- $needLinked[] = $contact;
- }
- }
- if ($needLinked) {
- foreach ($needLinked as $contact) {
- try {
- $this->Contacts->ContactLists->Contacts->link($this->Contacts->ContactLists->get($list), [$contact]);
- } catch (throwable $e) {
- $message = $e->getMessage() . PHP_EOL;
- $message .= "Contact: " . $contact->first_name . " " . $contact->last_name . PHP_EOL;
- $contact_list = $this->Contacts->ContactLists->get($list);
- $message .= "Contact List: " . $contact_list->name;
- $this->log($message);
- $this->err($message);
- return false;
- }
- }
- }
- }
- return true;
- }
- return false;
- } else {
- return true;
- }
- } else {
- return true;
- }
- }
- /**
- * @param $new
- * @param $contacts
- * @return mixed
- */
- protected function addToContactArray($new, $contacts)
- {
- if (!$new->hasErrors()) {
- $contacts[] = $new;
- } else {
- $message = "Cannot Save Contact" . PHP_EOL;
- foreach ($new->getErrors() as $field => $errorType) {
- foreach ($errorType as $errorName => $errorMsg) {
- $message .= "${errorName} Error on ${field}: $errorMsg " . PHP_EOL;
- }
- }
- $this->err($message);
- $this->log($message, 'error');
- }
- return $contacts;
- }
- /**
- * Get all new contacts from submission.
- *
- * @param array $submissions All submissions to be evaluated and sorted.
- * @param int $user_id ID of the user who submitted the contact data.
- * @return array All new contacts without any duplication.
- */
- public function getNewContacts(array $submissions, int $user_id): array
- {
- $results = [];
- $submissions = $this->removeDuplicates($submissions);
- foreach ($submissions as $submission) {
- if (!$this->submissionExists($submission, $user_id)) {
- $results[] = $submission;
- }
- }
- return $results;
- }
- /**
- * @param array $submissions
- * @param int $user_id
- * @param $data
- * @return bool
- * @throws \Exception
- */
- public function saveAllNewContacts(array $submissions, int $user_id, $data): bool
- {
- $newContacts = $this->getNewContacts($submissions, $user_id);
- if ($newContacts) {
- $contacts = [];
- foreach ($newContacts as $contact) {
- $new = $this->Contacts->newEntity($contact);
- if (!$new->can_text) {
- $new->can_text = false;
- }
- if (!$new->notes) {
- $new->notes = null;
- }
- if ($new->phone) {
- $new->phone = $this->cleanPhone($new->phone);
- }
- $contacts = $this->addToContactArray($new, $contacts);
- }
- if ($contacts) {
- if ($this->Contacts->saveMany($contacts, ['associated' => ['Companies']])) {
- foreach ($data['listIDs'] as $listID) {
- foreach ($contacts as $contact) {
- try {
- $this->Contacts->ContactLists->Contacts->link($this->Contacts->ContactLists->get($listID), [$contact]);
- } catch (throwable $e) {
- $message = $e->getMessage() . "EOL";
- $message .= "Contact: " . $contact->first_name . " " . $contact->last_name . PHP_EOL;
- $contact_list = $this->Contacts->ContactLists->get($listID);
- $message .= "Contact List: " . $contact_list->name;
- $this->log($message);
- $this->err($message);
- return false;
- }
- }
- }
- return true;
- } else {
- return false;
- }
- } else {
- return true;
- }
- } else {
- return true;
- }
- }
- /**
- * Add contact information to the contact entities array.
- *
- * @param array $row Contact information to be added.
- * @param \App\Model\Entity\User $user User associated with the contact.
- */
- public function addToEntities(array $row, User $user): void
- {
- $contacts = $this->Contacts->find()->where([
- 'AND' => [
- 'first_name' => trim($row['first_name']),
- 'last_name' => trim($row['last_name']),
- 'user_id' => $user->id,
- ],
- ]);
- if ($contacts->count() === 0) {
- $newContact = $this->Contacts->newEntity($row);
- if (!$newContact->hasErrors()) {
- $this->contactEntities[] = $newContact;
- }
- $this->newCount++;
- } else {
- foreach ($contacts as $contact) {
- if ($user->id === $contact->user_id) {
- $contact = $this->Contacts->patchEntity($contact, $row);
- if (!$contact->hasErrors()) {
- $this->contactEntities[] = $contact;
- $this->updateCount++;
- }
- }
- }
- }
- }
- /**
- * @inheritDoc
- */
- public function add()
- {
- $this->QueuedJobs->createJob('AddContacts');
- $this->success('OK, job created, now run the worker');
- }
- /**
- * Create new company.
- *
- * @param string $name Name of the company.
- * @param string|int $user_id ID of the user with which the company needs to be associated.
- * @return \App\Model\Entity\Company|bool|\App\Shell\Task\EntityInterface
- */
- public function createNewCompany(string $name, $user_id)
- {
- $data = [
- 'user_id' => $user_id,
- 'name' => $name,
- ];
- $company = $this->Contacts->Companies->newEntity($data);
- if ($this->Companies->save($company)) {
- return $company;
- }
- return false;
- }
- /**
- * Gets a header into a lower case underscored form.
- *
- * @param string $header Header to be processed.
- * @return string Formatted version of header.
- */
- protected function formatHeader(string $header): string
- {
- return strtolower(str_replace(' ', '_', $header));
- }
- /**
- * @param array $data
- * @param string|int $user_id
- * @return \App\Model\Entity\Contact|bool
- */
- public function findExisting(array $data, $user_id)
- {
- if (isset($data['first_name']) && isset($data['last_name'])) {
- $results = $this->Contacts->find()->where(
- [
- 'first_name' => $data['first_name'],
- 'last_name' => $data['last_name'],
- 'user_id' => $user_id,
- ]
- );
- /** @var \App\Model\Entity\Contact $contact */
- foreach ($results as $contact) {
- if ($this->isFullMatch($contact, $data)) {
- return $contact;
- }
- }
- return false;
- } else {
- throw new InvalidArgumentException('First and last names must be defined.');
- }
- }
- /**
- * Check if the data is a full match.
- * @param \App\Model\Entity\Contact $contact
- * @param array $data
- * @return bool
- */
- public function isFullMatch(Contact $contact, array $data): bool
- {
- foreach ($data as $key => $value) {
- if ($contact->$key !== $value) {
- return false;
- }
- }
- return true;
- }
- /**
- * Check if contact already exists in the contact list.
- *
- * @param string|int $contact_id
- * @param string|int $contact_list_id
- * @return bool
- */
- public function isContactInList($contact_id, $contact_list_id): bool
- {
- $reference = $this->Contacts->ContactToLists->find()->where(
- [
- 'contact_id' => $contact_id,
- 'contact_list_id' => $contact_list_id,
- ]
- );
- if ($reference->isEmpty()) {
- return false;
- } else {
- return true;
- }
- }
- /**
- * Clean out the phone number.
- * Remove all symbols.
- * Clear out any spaces between numbers.
- * Remove preceding number 1.
- *
- * @param string $phone Phone number to be cleaned
- * @return string Cleaned phone number.
- */
- public function cleanPhone(string $phone): string
- {
- // Expression Configuration
- $pattern = "/[^0-9]+/";
- $replacement = "";
- $charList = "1";
- // Expression Execution
- return ltrim(preg_replace($pattern, $replacement, $phone), $charList);
- }
- /**
- * Verify phone number is valid.
- * @param string $phone
- * @return bool
- */
- public function validatePhone(string $phone): bool
- {
- $numbersOnly = $this->cleanPhone($phone);
- $digitCount = strlen($numbersOnly);
- if ($phone !== $numbersOnly) {
- return false;
- }
- if ($digitCount !== 7 || $digitCount !== 10) {
- return false;
- }
- return true;
- }
- public function verifyPhone(string $phone): string
- {
- }
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement