| @@ -3,21 +3,20 @@ | ||
| 3 | 3 | namespace FluentSupport\App\Models\Traits; |
| 4 | 4 | |
| 5 | 5 | use Exception; |
| 6 | 6 | use FluentSupport\App\Models\Ticket; |
| 7 | -use FluentSupport\App\Models\Conversation; | |
| 8 | 7 | use FluentSupport\App\Services\Helper; |
| 9 | 8 | use FluentSupport\Framework\Support\Arr; |
| 10 | 9 | use FluentSupport\App\Services\ProfileInfoService; |
| 11 | -use FluentSupport\App\Services\Tickets\AgentTicketAccess; | |
| 12 | -use FluentSupport\App\Services\Tickets\TicketService; | |
| 13 | 10 | |
| 14 | 11 | trait CustomerTrait |
| 15 | 12 | { |
| 16 | 13 | /** |
| 17 | - * Identity columns that must never be mass-assigned on update. The | |
| 18 | - * `creating` hook sets them on create, but update() bypasses it. | |
| 14 | + * Identity columns that must never be mass-assigned on update. | |
| 19 | 15 | * |
| 16 | + * On create these are forced by the model's `creating` hook, but `update()` | |
| 17 | + * bypasses that hook, so they have to be filtered out here instead. | |
| 18 | + * | |
| 20 | 19 | * @var array |
| 21 | 20 | */ |
| 22 | 21 | private static $immutableUpdateKeys = [ |
| 23 | 22 | 'hash', // portal identity token |
| @@ -25,16 +24,22 @@ | ||
| 25 | 24 | 'person_type', // customer/agent record type, enforces the global scope |
| 26 | 25 | ]; |
| 27 | 26 | |
| 28 | 27 | /** |
| 29 | - * Whether this customer is barred from support altogether. Only 'active' | |
| 30 | - * is allowed, so any new status blocks until explicitly permitted. | |
| 28 | + * Whether this customer is barred from support altogether. | |
| 31 | 29 | * |
| 30 | + * Status is read as an allow list rather than a test for 'inactive'. The | |
| 31 | + * codebase previously mixed `!== 'active'` and `== 'inactive'` across eight | |
| 32 | + * call sites, so a status added later would have blocked some paths and | |
| 33 | + * silently passed others. With an allow list, any new status blocks | |
| 34 | + * everywhere until it is explicitly permitted. | |
| 35 | + * | |
| 32 | 36 | * @return bool |
| 33 | 37 | */ |
| 34 | 38 | public function isBlocked() |
| 35 | 39 | { |
| 36 | - // Some imported rows have an empty status. Treat it as the column default. | |
| 40 | + // Some imported rows carry an empty status. The column default is | |
| 41 | + // 'active', so read empty the same way instead of locking them out. | |
| 37 | 42 | $status = $this->status ?: 'active'; |
| 38 | 43 | |
| 39 | 44 | return $status !== 'active'; |
| 40 | 45 | } |
| @@ -39,11 +44,17 @@ | ||
| 39 | 44 | return $status !== 'active'; |
| 40 | 45 | } |
| 41 | 46 | |
| 42 | 47 | /** |
| 43 | - * Records that no WordPress account is linked to. Integrations can store | |
| 44 | - * 0 instead of NULL, so both count as unlinked. | |
| 48 | + * Records that no WordPress account is linked to. | |
| 45 | 49 | * |
| 50 | + * Asked in several places -- adopting records at registration, finding the | |
| 51 | + * records an address change stranded -- so the definition lives here rather | |
| 52 | + * than being spelled out at each call site. Unlinked reads as NULL or 0: | |
| 53 | + * the column is nullable, but data assembled from an integration can carry | |
| 54 | + * a zero, and a record that answered "unlinked" in one place and "linked" | |
| 55 | + * in another is how identity bugs start. | |
| 56 | + * | |
| 46 | 57 | * @param \FluentSupport\Framework\Database\Orm\Builder $query |
| 47 | 58 | * @return \FluentSupport\Framework\Database\Orm\Builder |
| 48 | 59 | */ |
| 49 | 60 | public function scopeUnclaimed($query) |
| @@ -74,12 +85,16 @@ | ||
| 74 | 85 | return !$this->isBlocked(); |
| 75 | 86 | } |
| 76 | 87 | |
| 77 | 88 | /** |
| 78 | - * Whether Fluent Support may send this customer a notification. Same check | |
| 79 | - * as portal access for now, kept separate so an email verification policy | |
| 80 | - * can change one without the other. | |
| 89 | + * Whether Fluent Support may send this customer a notification. | |
| 81 | 90 | * |
| 91 | + * Deliberately a separate question from portal access even though both | |
| 92 | + * currently reduce to the same status check. Reaching the portal is about | |
| 93 | + * who is asking; sending mail is about whether the address on file is one | |
| 94 | + * we should be writing to. An email verification policy would change this | |
| 95 | + * one without touching the other. | |
| 96 | + * | |
| 82 | 97 | * @return bool |
| 83 | 98 | */ |
| 84 | 99 | public function canReceiveNotifications() |
| 85 | 100 | { |
| @@ -86,8 +101,9 @@ | ||
| 86 | 101 | return !$this->isBlocked(); |
| 87 | 102 | } |
| 88 | 103 | |
| 89 | 104 | /** |
| 105 | + * This getCustomers method will return all customers | |
| 90 | 106 | * @param string $search |
| 91 | 107 | * @param string $status |
| 92 | 108 | * @return object |
| 93 | 109 | */ |
| @@ -225,12 +241,12 @@ | ||
| 225 | 241 | $addressFields = apply_filters('fluent_support/custom_registration_form_fields',$addressFields); |
| 226 | 242 | |
| 227 | 243 | foreach ($addressFields as $key => $field) { |
| 228 | 244 | if (isset($field['type']) && $field['type'] === 'country-selector') { |
| 229 | - continue; | |
| 245 | + continue; // Skip conversion for country-selector | |
| 230 | 246 | } |
| 231 | 247 | |
| 232 | - // _formBuilder.vue renders custom fields as 'input-text' with the real type in data_type. | |
| 248 | + //To create a custom field using _formBuilder.vue, the 'type' should be 'input-text', and the 'data_type' may vary, such as (text, number, date...). | |
| 233 | 249 | $addressFields[$key]['data_type'] = $addressFields[$key]['type']; |
| 234 | 250 | $addressFields[$key]['type'] = 'input-text'; |
| 235 | 251 | } |
| 236 | 252 | } |
| @@ -247,10 +263,9 @@ | ||
| 247 | 263 | |
| 248 | 264 | } |
| 249 | 265 | |
| 250 | 266 | /** |
| 251 | - * Create a customer from the WP user with this email. | |
| 252 | - * | |
| 267 | + * This `findUserInWPAndMakeCustomer` method will find customer in WP Users and make it as a customer | |
| 253 | 268 | * @param string $search |
| 254 | 269 | * @return object |
| 255 | 270 | */ |
| 256 | 271 | public function findUserInWPAndMakeCustomer ( $search ) |
| @@ -272,10 +287,9 @@ | ||
| 272 | 287 | } |
| 273 | 288 | } |
| 274 | 289 | |
| 275 | 290 | /** |
| 276 | - * Create a customer from the FluentCRM contact with this email. | |
| 277 | - * | |
| 291 | + * This `findInCrmAndMakeCustomer` method will find customer in Fluent CRM and make it as a customer | |
| 278 | 292 | * @param string $search |
| 279 | 293 | * @return object |
| 280 | 294 | */ |
| 281 | 295 | public function findInCrmAndMakeCustomer ( $search ) |
| @@ -292,8 +306,9 @@ | ||
| 292 | 306 | } |
| 293 | 307 | } |
| 294 | 308 | |
| 295 | 309 | /** |
| 310 | + * This getCustomer method will return a single customer | |
| 296 | 311 | * @param int $id |
| 297 | 312 | * @param array $with |
| 298 | 313 | * @return object |
| 299 | 314 | */ |
| @@ -313,8 +328,9 @@ | ||
| 313 | 328 | return $this->getCustomerAdditionalData($with, $customer, $data); |
| 314 | 329 | } |
| 315 | 330 | |
| 316 | 331 | /** |
| 332 | + * This createCustomer method will create a new customer | |
| 317 | 333 | * @param array $data |
| 318 | 334 | * @return object |
| 319 | 335 | */ |
| 320 | 336 | |
| @@ -321,12 +337,8 @@ | ||
| 321 | 337 | public function createCustomer($data) |
| 322 | 338 | { |
| 323 | 339 | $data = Arr::only($data, $this->getFillable()); |
| 324 | 340 | |
| 325 | - // Link a WP user only through the email, never a client user_id, or a new | |
| 326 | - // record could become another account's portal identity. | |
| 327 | - unset($data['user_id']); | |
| 328 | - | |
| 329 | 341 | $user = get_user_by('email', $data['email']); |
| 330 | 342 | |
| 331 | 343 | if ($user) { |
| 332 | 344 | $data['user_id'] = $user->ID; |
| @@ -341,8 +353,9 @@ | ||
| 341 | 353 | return static::create($data); |
| 342 | 354 | } |
| 343 | 355 | |
| 344 | 356 | /** |
| 357 | + * This updateCustomer method will update a customer | |
| 345 | 358 | * @since 1.5.7 |
| 346 | 359 | * @param int $customerId |
| 347 | 360 | * @param array $data |
| 348 | 361 | * @return object |
| @@ -349,9 +362,10 @@ | ||
| 349 | 362 | */ |
| 350 | 363 | |
| 351 | 364 | public function updateCustomer($customerId, $data) |
| 352 | 365 | { |
| 353 | - $customFieldsKeys = Helper::getCustomRegistrationFieldKeys(); | |
| 366 | + $customFieldsKeys = apply_filters('fluent_support/custom_registration_form_fields_key', Helper::getBusinessSettings('custom_registration_form_field')); | |
| 367 | + $customFieldsKeys = is_array($customFieldsKeys) ? $customFieldsKeys : []; | |
| 354 | 368 | $customFormValue = Arr::only($data, $customFieldsKeys); |
| 355 | 369 | |
| 356 | 370 | $data = $this->takeValidKeysForUpdate($data); |
| 357 | 371 | |
| @@ -368,17 +382,20 @@ | ||
| 368 | 382 | |
| 369 | 383 | $user = get_user_by('email', $data['email']); |
| 370 | 384 | |
| 371 | 385 | if ($user && !empty($customFieldsKeys)) { |
| 372 | - // The submitted email alone must not re-bind this record to the account that owns it. | |
| 386 | + // Never trust the submitted email to pick the account: an email edit | |
| 387 | + // must not silently re-bind this record to whoever owns that address. | |
| 373 | 388 | $linkedUserId = $this->resolveUserLinkage($customer, (int) $user->ID); |
| 374 | 389 | |
| 375 | 390 | if ($linkedUserId) { |
| 376 | 391 | $data['user_id'] = $linkedUserId; |
| 377 | 392 | |
| 393 | + //Update Custom field data for user | |
| 378 | 394 | foreach ($customFieldsKeys as $key) { |
| 379 | - if (isset($customFormValue[$key]) && is_scalar($customFormValue[$key])) { | |
| 380 | - update_user_meta($linkedUserId, $key, sanitize_text_field($customFormValue[$key])); | |
| 395 | + if (isset($customFormValue[$key])) { | |
| 396 | + $fieldValue = $customFormValue[$key]; | |
| 397 | + update_user_meta($linkedUserId, $key, $fieldValue); | |
| 381 | 398 | } |
| 382 | 399 | } |
| 383 | 400 | } |
| 384 | 401 | } |
| @@ -388,10 +405,17 @@ | ||
| 388 | 405 | static::where('id', $customer->id)->update($data); |
| 389 | 406 | |
| 390 | 407 | $updated = static::findOrFail($customerId); |
| 391 | 408 | |
| 392 | - // Revoke signed ticket links sent to the old address. The update above | |
| 393 | - // is a query-builder write and fires no model events, so do it here. | |
| 409 | + // An agent moving the address has the same consequence as any other | |
| 410 | + // proven move, and this was the one path that skipped it. Signed ticket | |
| 411 | + // links already delivered to the previous inbox keep authorising read, | |
| 412 | + // reply, close and reopen until the hash they carry stops matching, so | |
| 413 | + // correcting a customer's address left whoever holds the old inbox with | |
| 414 | + // working access to every one of their tickets. | |
| 415 | + // | |
| 416 | + // The update above is a query-builder write, which fires no model | |
| 417 | + // events, so this cannot be left to Ticket::updating either. | |
| 394 | 418 | if (strtolower(trim($previousEmail)) !== strtolower(trim((string) $updated->email))) { |
| 395 | 419 | ProfileInfoService::onProvenEmailChange($updated, $previousEmail, 'agent'); |
| 396 | 420 | } |
| 397 | 421 | |
| @@ -398,10 +422,9 @@ | ||
| 398 | 422 | return $updated; |
| 399 | 423 | } |
| 400 | 424 | |
| 401 | 425 | /** |
| 402 | - * Delete a customer and all their tickets. | |
| 403 | - * | |
| 426 | + * deleteCustomer method will delete a customer and all tickets by that customer | |
| 404 | 427 | * @since 1.5.7 |
| 405 | 428 | * @param int $customerId |
| 406 | 429 | * @return array |
| 407 | 430 | */ |
| @@ -407,34 +430,25 @@ | ||
| 407 | 430 | */ |
| 408 | 431 | |
| 409 | 432 | public function deleteCustomer($customerId) |
| 410 | 433 | { |
| 411 | - $this->deleteWithTickets(static::findOrFail($customerId)); | |
| 434 | + $customer = static::findOrFail($customerId); | |
| 412 | 435 | |
| 413 | - return [ | |
| 414 | - 'message' => __('Customer Deleted Successfully', 'fluent-support') | |
| 415 | - ]; | |
| 416 | - } | |
| 436 | + $tickets = Ticket::where('customer_id', $customer->id)->get(); | |
| 417 | 437 | |
| 418 | - /** | |
| 419 | - * Delete a customer after their tickets. Tickets go through TicketService | |
| 420 | - * so `fluent_support/ticket_deleted` fires, as for any other ticket delete. | |
| 421 | - * | |
| 422 | - * @param \FluentSupport\App\Models\Customer $customer | |
| 423 | - * @return void | |
| 424 | - */ | |
| 425 | - protected function deleteWithTickets($customer) | |
| 426 | - { | |
| 427 | - $ticketService = new TicketService(); | |
| 428 | - | |
| 429 | - foreach (Ticket::where('customer_id', $customer->id)->get() as $ticket) { | |
| 430 | - $ticketService->deleteTicket($ticket); | |
| 438 | + foreach ($tickets as $ticket) { | |
| 439 | + $ticket->deleteTicket(); | |
| 431 | 440 | } |
| 432 | 441 | |
| 433 | 442 | $customer->delete(); |
| 443 | + | |
| 444 | + return [ | |
| 445 | + 'message' => __('Customer Deleted Successfully', 'fluent-support') | |
| 446 | + ]; | |
| 434 | 447 | } |
| 435 | 448 | |
| 436 | 449 | /** |
| 450 | + * bulkDeleteCustomers method will delete multiple customers and all their tickets | |
| 437 | 451 | * @since 1.9.3 |
| 438 | 452 | * @param array $customerIds |
| 439 | 453 | * @return array |
| 440 | 454 | */ |
| @@ -450,9 +464,17 @@ | ||
| 450 | 464 | $errors = []; |
| 451 | 465 | |
| 452 | 466 | foreach ($customerIds as $customerId) { |
| 453 | 467 | try { |
| 454 | - $this->deleteWithTickets(static::findOrFail($customerId)); | |
| 468 | + $customer = static::findOrFail($customerId); | |
| 469 | + | |
| 470 | + $tickets = Ticket::where('customer_id', $customer->id)->get(); | |
| 471 | + | |
| 472 | + foreach ($tickets as $ticket) { | |
| 473 | + $ticket->deleteTicket(); | |
| 474 | + } | |
| 475 | + | |
| 476 | + $customer->delete(); | |
| 455 | 477 | $deletedCount++; |
| 456 | 478 | |
| 457 | 479 | } catch (\Exception $e) { |
| 458 | 480 | /* translators: %1$d: customer ID, %2$s: error message */ |
| @@ -460,11 +482,11 @@ | ||
| 460 | 482 | } |
| 461 | 483 | } |
| 462 | 484 | |
| 463 | 485 | if ($deletedCount > 0) { |
| 486 | + /* translators: %1$d: number of customers deleted */ | |
| 464 | 487 | $message = $deletedCount === 1 |
| 465 | 488 | ? __('Customer deleted successfully', 'fluent-support') |
| 466 | - /* translators: %1$d: number of customers deleted */ | |
| 467 | 489 | : sprintf(__('%1$d customers deleted successfully', 'fluent-support'), $deletedCount); |
| 468 | 490 | } else { |
| 469 | 491 | $message = __('No customers were deleted', 'fluent-support'); |
| 470 | 492 | } |
| @@ -482,10 +504,9 @@ | ||
| 482 | 504 | return $response; |
| 483 | 505 | } |
| 484 | 506 | |
| 485 | 507 | /** |
| 486 | - * Attach ticket/response counts and profile link. | |
| 487 | - * | |
| 508 | + * This attachCustomersMetaData method will attach meta data to customers | |
| 488 | 509 | * @since 1.5.7 |
| 489 | 510 | * @param object $customers |
| 490 | 511 | * @return object |
| 491 | 512 | */ |
| @@ -490,39 +511,13 @@ | ||
| 490 | 511 | * @return object |
| 491 | 512 | */ |
| 492 | 513 | private function attachCustomersMetaData($customers) |
| 493 | 514 | { |
| 494 | - $customerIds = []; | |
| 495 | 515 | foreach ($customers as $customer) { |
| 496 | - $customerIds[] = $customer->id; | |
| 497 | - } | |
| 498 | - | |
| 499 | - $ticketCounts = []; | |
| 500 | - $responseCounts = []; | |
| 501 | - | |
| 502 | - if ($customerIds) { | |
| 503 | - // Count only tickets the current agent may open. | |
| 504 | - $access = new AgentTicketAccess(); | |
| 505 | - | |
| 506 | - $ticketsQuery = Ticket::select('customer_id') | |
| 507 | - ->selectRaw('COUNT(*) as total') | |
| 508 | - ->whereIn('customer_id', $customerIds) | |
| 509 | - ->groupBy('customer_id'); | |
| 510 | - $access->applyAccessScope($ticketsQuery); | |
| 511 | - $ticketCounts = $ticketsQuery->pluck('total', 'customer_id')->all(); | |
| 512 | - | |
| 513 | - $responsesQuery = Conversation::select('person_id') | |
| 514 | - ->selectRaw('COUNT(*) as total') | |
| 515 | - ->whereIn('person_id', $customerIds) | |
| 516 | - ->groupBy('person_id'); | |
| 517 | - $access->applyAccessScopeViaTicket($responsesQuery); | |
| 518 | - $responseCounts = $responsesQuery->pluck('total', 'person_id')->all(); | |
| 519 | - } | |
| 520 | - | |
| 521 | - foreach ($customers as $customer) { | |
| 522 | - $customer->total_tickets = (int) Arr::get($ticketCounts, $customer->id, 0); | |
| 523 | - $customer->total_responses = (int) Arr::get($responseCounts, $customer->id, 0); | |
| 516 | + $customer->total_tickets = $customer->getTicketCounts(); | |
| 517 | + $customer->total_responses = $customer->getResponseCounts(); | |
| 524 | 518 | if ($customer->user_id) { |
| 519 | + //Get profile link, if they are WP user | |
| 525 | 520 | $customer->user_profile = admin_url('user-edit.php?user_id=' . $customer->user_id); |
| 526 | 521 | } |
| 527 | 522 | } |
| 528 | 523 | |
| @@ -529,8 +524,9 @@ | ||
| 529 | 524 | return $customers; |
| 530 | 525 | } |
| 531 | 526 | |
| 532 | 527 | /** |
| 528 | + * This getCustomerAdditionalData method will return additional data for a customer | |
| 533 | 529 | * @since 1.5.7 |
| 534 | 530 | * @param array $with |
| 535 | 531 | * @param object $customer |
| 536 | 532 | * @param array $data |
| @@ -542,9 +538,9 @@ | ||
| 542 | 538 | $customFieldKeys = apply_filters('fluent_support/custom_registration_form_fields_key', []); |
| 543 | 539 | |
| 544 | 540 | $userMetaData = get_user_meta($customer['user_id']); |
| 545 | 541 | |
| 546 | - // Custom field values live in WP user meta | |
| 542 | + //Custom field data from wp user_meta | |
| 547 | 543 | foreach ($customFieldKeys as $fieldKey) { |
| 548 | 544 | if (isset($userMetaData[$fieldKey][0])) { |
| 549 | 545 | $customer[$fieldKey] = $userMetaData[$fieldKey][0]; |
| 550 | 546 | } |
| @@ -561,15 +557,10 @@ | ||
| 561 | 557 | * @param int $limit |
| 562 | 558 | */ |
| 563 | 559 | $limit = apply_filters('fluent_support/customer_page_ticket_widgets_limit', 20); |
| 564 | 560 | |
| 565 | - $ticketsQuery = Ticket::select(['id', 'title', 'status', 'customer_id', 'created_at']) | |
| 566 | - ->where('customer_id', $customer->id); | |
| 567 | - | |
| 568 | - // Only list tickets this agent may open (visibility + mailbox restrictions). | |
| 569 | - (new AgentTicketAccess())->applyAccessScope($ticketsQuery); | |
| 570 | - | |
| 571 | - $data['tickets'] = $ticketsQuery | |
| 561 | + $data['tickets'] = Ticket::select(['id', 'title', 'status', 'customer_id', 'created_at']) | |
| 562 | + ->where('customer_id', $customer->id) | |
| 572 | 563 | ->latest('id') |
| 573 | 564 | ->limit($limit) |
| 574 | 565 | ->get(); |
| 575 | 566 | } |
| @@ -581,10 +572,9 @@ | ||
| 581 | 572 | return $data; |
| 582 | 573 | } |
| 583 | 574 | |
| 584 | 575 | /** |
| 585 | - * Another customer with this email, or false. | |
| 586 | - * | |
| 576 | + * This customerExists method will check if customer exists | |
| 587 | 577 | * @since 1.5.7 |
| 588 | 578 | * @param int $customerId |
| 589 | 579 | * @param string $email |
| 590 | 580 | * @return mixed |
| @@ -600,13 +590,17 @@ | ||
| 600 | 590 | } |
| 601 | 591 | |
| 602 | 592 | |
| 603 | 593 | /** |
| 604 | - * Which WP account this customer may be linked to. The portal resolves | |
| 605 | - * customers by user_id, so re-binding hands over their tickets. Only admins | |
| 606 | - * may re-bind, and not to an account another customer already owns. | |
| 607 | - * Otherwise the existing link is kept. | |
| 594 | + * Decide which WordPress account this customer record may be linked to. | |
| 608 | 595 | * |
| 596 | + * Binding a support record to a WP account is an identity change: the portal | |
| 597 | + * resolves the logged-in customer by `user_id` (see Helper::getCurrentCustomer), | |
| 598 | + * so a re-bind hands this record's tickets to the newly bound account. It is | |
| 599 | + * therefore restricted to administrators and refused when the account already | |
| 600 | + * belongs to another customer. When a re-bind is not allowed the existing | |
| 601 | + * linkage is kept, so ordinary email corrections still go through. | |
| 602 | + * | |
| 609 | 603 | * @param object $customer |
| 610 | 604 | * @param int $targetUserId WP user matching the submitted email |
| 611 | 605 | * @return int user id to link, or 0 for none |
| 612 | 606 | */ |
| @@ -613,8 +607,9 @@ | ||
| 613 | 607 | private function resolveUserLinkage($customer, $targetUserId) |
| 614 | 608 | { |
| 615 | 609 | $currentUserId = (int) $customer->user_id; |
| 616 | 610 | |
| 611 | + // Already linked to this account, nothing to authorize. | |
| 617 | 612 | if ($targetUserId === $currentUserId) { |
| 618 | 613 | return $currentUserId; |
| 619 | 614 | } |
| 620 | 615 | |
| @@ -621,8 +616,9 @@ | ||
| 621 | 616 | if (!current_user_can('manage_options')) { |
| 622 | 617 | return $currentUserId; |
| 623 | 618 | } |
| 624 | 619 | |
| 620 | + // Conflict: that account is already bound to a different customer record. | |
| 625 | 621 | $alreadyLinked = static::where('id', '!=', $customer->id) |
| 626 | 622 | ->where('user_id', $targetUserId) |
| 627 | 623 | ->first(); |
| 628 | 624 | |
| @@ -633,8 +629,9 @@ | ||
| 633 | 629 | return $targetUserId; |
| 634 | 630 | } |
| 635 | 631 | |
| 636 | 632 | /** |
| 633 | + * This takeValidKeysForUpdate method will take valid keys data for update | |
| 637 | 634 | * @since 1.5.7 |
| 638 | 635 | * @param array $data |
| 639 | 636 | * @return array |
| 640 | 637 | */ |
| @@ -639,8 +636,10 @@ | ||
| 639 | 636 | * @return array |
| 640 | 637 | */ |
| 641 | 638 | private function takeValidKeysForUpdate($data) |
| 642 | 639 | { |
| 640 | + // $fillable is a numerically indexed list, so the immutable columns are | |
| 641 | + // values rather than keys and have to be removed with array_diff(). | |
| 643 | 642 | $validKeys = array_diff($this->getFillable(), self::$immutableUpdateKeys); |
| 644 | 643 | |
| 645 | 644 | return Arr::only($data, $validKeys); |
| 646 | 645 | } |