Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

xiolog's avatar

How can I refactor this code?

Hi all.

I have a table of cities in the database.

I need to generate an array of the following form:

[
	['A'] => [
		18 => 'Alexandria',
		24 => 'Antioch',
		1679 => 'Akron',
		1875 => 'Atlanta',
		2713 => 'Abilene',	
	],
	['B'] => [
		28 => 'Bakersfield',
		154 => 'Billings',
		1495 => 'Bridgeport',
		2150 => 'Baltimore',
		4117 => 'Bend',	
	]
]

I tried to write the code myself, but it turned out to be big. I'm sure there is a more succinct solution.

$cities = City::pluck('title', 'id');

$letters = $cities->sort()->map(function($value) {
	return Str::limit($value, 1, '');
})->unique()->flatten();

$lettersArray = [];

foreach ($letters as $letter) {
	foreach($cities as $key => $city) {
		if ($letter == Str::limit($city, 1, '')) {
			$lettersArray[$letter][$key] = $city;
		}
	}
}

How can I refactor it?

And I also need to sort the cities by name within the array. I don't understand how to do it. sort() doesn't work.

0 likes
27 replies
kodyxgen's avatar
kodyxgen
Best Answer
Level 51

Hi, could something like this work for you ?

$sortedCities = $cities->groupBy(function($city,$id){

	return Str::limit($city,1,'');
        
}, true)->unique();
2 likes
kodyxgen's avatar

Or this, it is shorter.

$sortedCities = $cities->sort()->groupBy(
    fn($city) => Str::limit($city,1,''), true)->toArray();
2 likes
xiolog's avatar

This is the best solution. Only the "pluck" method already creates the collection and there is no need to reuse the "collect" method.

1 like
kodyxgen's avatar

Yes you are right! in my first post i didn't add the collect(), after that i was not sure if pluck() is returning a collection or and array so i edited the post.

i will edit the post now so it will be correct.

bugsysha's avatar
$result = City::pluck('title', 'id')
	->unique()
	->groupBy(function (string $title): string {
		return strtolower($title[0]);
	});

Or if on PHP with short closures:

$result = City::pluck('title', 'id')->unique()->groupBy(fn (string $title): string => strtolower($title[0])});

Just make sure you convert the title to lower or upper case cause you might have A and a as array keys.

2 likes
xiolog's avatar

Thanks for the idea of converting the case of letters in an array. I will make a check.

bugsysha's avatar

You are very welcome. Just make sure that you always use same case and performance of $title[0] is far better than anything else to get the first letter. Especially better than what you used Str::limit($city, 1, '').

xiolog's avatar

I'm implementing that right now. Your solution didn't work for me the first time. There is only 1 element of the array left.

bugsysha's avatar

Maybe unique() is not required. I assume you don't duplicate cities if you've normalized the data in the table. What was the error?

kodyxgen's avatar

Try adding true as a second argument for groupBy, that will return the id to.

Maybe best of all is :

$sortedCities = $cities->sort()->unique()
    ->groupBy(fn( string $city): string => strtolower(trim($city)[0]), true);

credits for @bugsysha for the array tip, he is right, it is better then Str::limit

xiolog's avatar

The problem is because of this part of the code:

return strtolower($title[0]);

I use the Cyrillic alphabet. Maybe because of that.

bugsysha's avatar

@xiolog I leave that to you to figure out. You now have performance and bug fix tips. Rest is up to you to pick and modify what suites you best.

xiolog's avatar

@bugsysha, @kodyxgen This code:

$cities = City::pluck('title', 'id')->sort()->unique()
->groupBy(function (string $city): string {
	return strtolower(trim($city)[0]);
}, true);

I get:

Illuminate\Support\Collection {#1713 ▼
  #items: array:1 [▼
    b"Ð" => Illuminate\Support\Collection {#1607 ▶}
  ]
}
xiolog's avatar

I'm looking for the right solution :)

kodyxgen's avatar

I only could make it work if i reverted back to Str::limit - since there is a problem from the Cyrillic alphabet.

$cities = $cities->sort()->unique()
->groupBy(function (string $city): string {
	return strtolower(Str::limit(trim($city),1,''));
}, true);

and i get

=> Illuminate\Support\Collection {#1395
     all: [
       "Б" => Illuminate\Support\Collection {#1397
         all: [
           28 => "Бakersfield",
           2150 => "Бaltimore",
           4117 => "Бend",
           154 => "Бillings",
           1495 => "Бridgeport",
         ],
       },
       "Д" => Illuminate\Support\Collection {#1396
         all: [
           2713 => "Дbilene",
           1679 => "Дkron",
           18000 => "Дlexandria",
           24 => "Дntioch",
           1875 => "Дtlanta",
         ],
       },
     ],
   }

1 like
xiolog's avatar

@bugsysha, @kodyxgen Thanks, guys. The problem turned out to be my Cyrillic encoding :)

How is $city[0] better than Str::limit($city, 1, '')?

xiolog's avatar

@bugsysha, @kodyxgen Thanks a lot. I will use this solution. I changed the best answer. You are the best. I wouldn't have thought of it myself. :)

kodyxgen's avatar

So you will use the Str::limit solution :)

1 like
kodyxgen's avatar

Then maybe my first answer could the best one in your particular case ;)

bugsysha's avatar

@xiolog I don't know how your app works, but if you use multiple encodings then consider having english title for all cities so you can have consistent results.

xiolog's avatar

This is not an option. I need Cyrillic

bugsysha's avatar

@xiolog not to replace the current. Create additional column. And then use that column to do this logic.

rodrigo.pedra's avatar
$cities = City::pluck('name', 'id')
  ->groupBy(fn($name): string => Str::of($name)->limit(1, '')->ascii()->upper(), true)
  ->map(fn($names) => $names->sort())
  ->sortKeys()
  ->toArray();
  • Sorts groups (letter keys)
  • Sorts names within groups
  • As you need Cyrillic, maybe you'll need to remove ->ascii() call. I added it so it removed accents, not sure if it works with Cyrillic.
  • I didn't add a ->unique() call, as I assumed that different ids would correspond to different cities even if they have the same name. If you want unique city names, add ->unique() right after ->sort() on the third line.

Please or to participate in this conversation.