Support the ongoing development of Laravel.io →
Database Eloquent
Last updated 1 year ago.
0

Hello, I ran into this issue today as well. You were the only other person I could find who experienced this, and of course there were no responses (after 1 year). I figured out what the problem was, and came up with some form of a work around, so hopefully I can shed some light on this for anyone else who may comes across this.

If you just want to resolve the issue, and don't care to understand, then create the following class:

<?php
use Illuminate\Database\Eloquent\Collection;

/**
 * Class HasMany_CaseInsensitive
 *
 * This relation class was the result of many frustrating hours of debugging a case sensitivty issue when eager-loading relationships.
 * I thought it was a case sensitivity issue with the SQL,
 * but as it turns out the problem was actually with eloquent performing lookups on array keys (which are case sensitive)
 *
 * SO, if you ever need to create a case insensitive relation that is keyed on string values you should use this relationship class.
 *
 * @author Matthew April
 */
class HasMany_CaseInsensitive extends Illuminate\Database\Eloquent\Relations\HasMany {

    /**
     * Match the eagerly loaded results to their many parents.
     *
     * @param  array   $models
     * @param  \Illuminate\Database\Eloquent\Collection  $results
     * @param  string  $relation
     * @param  string  $type
     * @return array
     */
    protected function matchOneOrMany(array $models, Collection $results, $relation, $type)
    {
        $dictionary = $this->buildDictionary($results);


        // Once we have the dictionary we can simply spin through the parent models to
        // link them up with their children using the keyed dictionary to make the
        // matching very convenient and easy work. Then we'll just return them.
        foreach ($models as $model)
        {
            //***** MA: retrieve value as lower case.
            $key = strtolower( $model->getAttribute($this->localKey) );
            if (isset($dictionary[$key]))
            {
                $value = $this->getRelationValue($dictionary, $key, $type);

                $model->setRelation($relation, $value);
            }
        }

        return $models;
    }

    /**
     * Build model dictionary keyed by the relation's foreign key.
     *
     * @param  \Illuminate\Database\Eloquent\Collection  $results
     * @return array
     */
    protected function buildDictionary(Collection $results)
    {
        $dictionary = array();

        $foreign = $this->getPlainForeignKey();

        // First we will create a dictionary of models keyed by the foreign key of the
        // relationship as this will allow us to quickly access all of the related
        // models without having to do nested looping which will be quite slow.
        foreach ($results as $result)
        {
            //****** MA: keyed on lower case value
            $dictionary[strtolower($result->{$foreign})][] = $result;
        }

        return $dictionary;
    }
}

And from within your model create a relation that returns the following:

public function allMarketingConsent() {
        $related = new Related();
        return new HasMany_CaseInsensitive($related->newQuery(), $this, $related->getTable().'.foreignKey', 'localKey');
    }

For HasOne, do the exact same just call it HasOne_CaseInsensitive

So, the problem I addressed with this code was the fact that the HasOneOrMany::matchOneOrMany() matches the results to the models by performing a lookup on the dictionary (array) keys, which are case sensitive in PHP. So the solution was to key the dictionary on the lower case values, and perform the lookup on the lower case attribute.

0

Hi Matt, I ran into this exact issue in Februrary (stack overflow post) and tracked it down to isset() function but never coded a real solution, instead I've used workarounds on a model by model basis. Thanks for posting your class I will certainly be using is as a basis to a more permanent solution to the issue!

I hope you don't mind Matt but I've built on your solution to provide a cleaner (in my opinion) way of accessing the case-insensitive relation.

Here's the new case insensitive class as you posted (but placed in \vendor\laravel\framework\src\Illuminate\Database\Eloquent\Relations\HasManyCI.php):

<?php namespace Illuminate\Database\Eloquent\Relations;

use Illuminate\Database\Eloquent\Collection;

class HasManyCI extends HasOneOrMany {

	/**
	 * Get the results of the relationship.
	 *
	 * @return mixed
	 */
	public function getResults()
	{
		return $this->query->get();
	}

	/**
	 * Initialize the relation on a set of models.
	 *
	 * @param  array   $models
	 * @param  string  $relation
	 * @return array
	 */
	public function initRelation(array $models, $relation)
	{
		foreach ($models as $model)
		{
			$model->setRelation($relation, $this->related->newCollection());
		}

		return $models;
	}

	/**
		* Build model dictionary keyed by the relation's foreign key.
		*
		* @param  \Illuminate\Database\Eloquent\Collection  $results
		* @return array
		*/
	 protected function buildDictionary(Collection $results)
	 {
			 $dictionary = array();

			 $foreign = $this->getPlainForeignKey();

			 // First we will create a dictionary of models keyed by the foreign key of the
			 // relationship as this will allow us to quickly access all of the related
			 // models without having to do nested looping which will be quite slow.
			 foreach ($results as $result)
			 {
					 $dictionary[strtolower($result->{$foreign})][] = $result;
			 }

			 return $dictionary;
	 }

	/**
		* Match the eagerly loaded results to their many parents.
		*
		* @param  array   $models
		* @param  \Illuminate\Database\Eloquent\Collection  $results
		* @param  string  $relation
		* @param  string  $type
		* @return array
		*/
	 protected function matchOneOrMany(array $models, Collection $results, $relation, $type)
	 {
			 $dictionary = $this->buildDictionary($results);


			 // Once we have the dictionary we can simply spin through the parent models to
			 // link them up with their children using the keyed dictionary to make the
			 // matching very convenient and easy work. Then we'll just return them.
			 foreach ($models as $model)
			 {
					 $key = strtolower( $model->getAttribute($this->localKey) );
					 if (isset($dictionary[$key]))
					 {
							 $value = $this->getRelationValue($dictionary, $key, $type);
							 $model->setRelation($relation, $value);
					 }
			 }

			 return $models;
	 }

	/**
	 * Match the eagerly loaded results to their parents.
	 *
	 * @param  array   $models
	 * @param  \Illuminate\Database\Eloquent\Collection  $results
	 * @param  string  $relation
	 * @return array
	 */
	public function match(array $models, Collection $results, $relation)
	{
		return $this->matchMany($models, $results, $relation);
	}

}

I then added a method to the Model class (\vendor\laravel\framework\src\Illuminate\Database\Eloquent\Model.php):

<?php
...
public function hasManyCI($related, $foreignKey = null, $localKey = null)
{
	$foreignKey = $foreignKey ?: $this->getForeignKey();

	$instance = new $related;

	$localKey = $localKey ?: $this->getKeyName();

	return new HasManyCI($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
}

not forgetting (use Illuminate\Database\Eloquent\Relations\HasManyCI;)

and you invoke the new relationship like:

public function delivery() {
	return $this->hasManyCI('App\Models\Delivery', 'delivery_address', 'address');
}
Last updated 7 years ago.
0

Has someone ported this solution to current laravel 5.4 in a way, it coexistent and are not overwritten by updates?

0

Sign in to participate in this thread!

Eventy

Your banner here too?

TeroBlaZe teroblaze Joined 26 Feb 2014

Moderators

We'd like to thank these amazing companies for supporting us

Your logo here?

Laravel.io

The Laravel portal for problem solving, knowledge sharing and community building.

© 2024 Laravel.io - All rights reserved.