Как получить неквалифицированное (короткое) имя класса объекта?


153

Как проверить класс объекта в среде PHP с пространством имен без указания полного класса пространства имен.

Например, предположим, у меня была библиотека объектов / Entity / Contract / Name.

Следующий код не работает, так как get_class возвращает полный класс пространства имен.

If(get_class($object) == 'Name') {
... do this ...
}

Волшебное ключевое слово пространства имен возвращает текущее пространство имен, которое бесполезно, если у тестируемого объекта есть другое пространство имен.

Я мог бы просто указать полное имя класса с пространствами имен, но это, кажется, блокирует структуру кода. Также не очень полезно, если я хотел динамически изменить пространство имен.

Может кто-нибудь придумать эффективный способ сделать это. Я думаю, что один из вариантов это регулярное выражение.


Это кажется почти бессмысленным, потому что разные пространства имен могут иметь одинаковые имена классов, определенные внутри них, так как вы справитесь с этим? И это потому, что в вашем образце возвращается полное имя класса
Alma Do

Я нахожусь на мобильном устройстве, поэтому я не могу представить достойный ответ, но решение - это отражение, в частности ReflectionClass :: getShortName - php.net/manual/en/reflectionclass.getshortname.php
lonesomeday

Для людей, ищущих причину, чтобы хотеть этого: это может быть полезно в вспомогательной функции в общем базовом классе (т.е. множественные пространства имен никогда не являются проблемой в этой ситуации).
Даррен Кук

Ответы:


182

Вы можете сделать это с отражением. В частности, вы можете использовать ReflectionClass::getShortNameметод, который получает имя класса без его пространства имен.

Сначала вам нужно создать ReflectionClassэкземпляр, а затем вызвать getShortNameметод этого экземпляра:

$reflect = new ReflectionClass($object);
if ($reflect->getShortName() === 'Name') {
    // do this
}

Однако я не могу представить много обстоятельств, когда это было бы желательно. Если вы хотите требовать, чтобы объект был членом определенного класса, способ проверить это с instanceof. Если вам нужен более гибкий способ сигнализации определенных ограничений, то для этого нужно написать интерфейс и потребовать, чтобы код реализовывал этот интерфейс. Опять же, правильный способ сделать это с instanceof. (Вы можете сделать это с ReflectionClass, но это будет иметь гораздо худшую производительность.)


1
@ Greg.Forbes Потому Tenantчто не существует в текущем пространстве имен. Попробуй var_dump($tenant instanceof \Library\Entity\People\Tenant)вместо этого. Кроме того, исследуйте, как использовать useоператор, и общую концепцию пространств имен PHP!
одинокий

3
Мне пришлось добавить косую черту впереди, как это$reflect = new \ReflectionClass($object);
prograhammer

7
Как правило, я не люблю много использовать ReflectionClass voodoo в своем приложении, потому что оно может привести к неожиданным результатам при неправильном использовании (защищенные методы становятся общедоступными и т. Д.). Вы можете использовать простую замену строки на магические константы PHP вместо: str_replace(__NAMESPACE__ . '\\', '', __CLASS__);. Это также намного быстрее, с точки зрения производительности.
Франклин П Струб

2
@FranklinPStrube Если я что-то упустил, то получаю короткое имя текущего класса, а не класс объекта. Я согласен, что использование отражения обычно означает, что вы делаете это неправильно.
одинокий

1
Многие люди используют Reflections для переопределения видимости члена, то есть BAD. Не делай этого! Но заявление о том, что использование Reflections в целом является вуду, а Doing It Wrong дает людям неправильное впечатление. Вы не должны избегать их, вы должны понимать их и знать, когда они полезны и на каком уровне абстракции.
Ваня Д.

131

(new \ReflectionClass($obj))->getShortName(); является лучшим решением в отношении производительности.

Мне было любопытно, какое из представленных решений является самым быстрым, поэтому я собрал небольшой тест.

Полученные результаты

Reflection: 1.967512512207 s ClassA
Basename:   2.6840535163879 s ClassA
Explode:    2.6507515668869 s ClassA

Код

namespace foo\bar\baz;

class ClassA{
    public function getClassExplode(){
        return explode('\\', static::class)[0];
    }

    public function getClassReflection(){
        return (new \ReflectionClass($this))->getShortName();
    }

    public function getClassBasename(){
        return basename(str_replace('\\', '/', static::class));
    }
}

$a = new ClassA();
$num = 100000;

$rounds = 10;
$res = array(
    "Reflection" => array(),
    "Basename" => array(),
    "Explode" => array(),
);

for($r = 0; $r < $rounds; $r++){

    $start = microtime(true);
    for($i = 0; $i < $num; $i++){
        $a->getClassReflection();
    }
    $end = microtime(true);
    $res["Reflection"][] = ($end-$start);

    $start = microtime(true);
    for($i = 0; $i < $num; $i++){
        $a->getClassBasename();
    }
    $end = microtime(true);
    $res["Basename"][] = ($end-$start);

    $start = microtime(true);
    for($i = 0; $i < $num; $i++){
        $a->getClassExplode();
    }
    $end = microtime(true);
    $res["Explode"][] = ($end-$start);
}

echo "Reflection: ".array_sum($res["Reflection"])/count($res["Reflection"])." s ".$a->getClassReflection()."\n";
echo "Basename: ".array_sum($res["Basename"])/count($res["Basename"])." s ".$a->getClassBasename()."\n";
echo "Explode: ".array_sum($res["Explode"])/count($res["Explode"])." s ".$a->getClassExplode()."\n";

Результаты на самом деле удивили меня. Я думал, что решение взрыва будет самым быстрым способом ...


1
Отличный ответ. Я выполнял тот же код, но получил другой результат (Macbook Pro i7, 16 ГБ ОЗУ). Отражение: 0.382, Базовое имя: 0.380, Взрыв: 0.399. Я думаю, это зависит от вашей системы, что лучше ...
Тобиас Нихольм

4
Запустите PHP 10 000 раз с этим кодом, и вы получите лучший результат. Вышеприведенное может извлечь отражение из некоторого пула, но это не обычное поведение приложений. Им это нужно только один или два раза.
LeMike

6
Интересно, выполняется ли этот тест при создании экземпляра ReflectionClass для более существенного объекта, чем маленький объект класса A в вашем тесте ...
Джо Грин

2
выполнение только одной итерации вместо 100000 дает совершенно другой результат: Отражение: 1.0967254638672 100000th / s ClassA Basename: 0.81062316894531 100000th / s ClassA Explode: 0.50067901611328 100000th / s ClassA
mcmurphy

1
взорваться ('\\', статический :: класс) [0]? не возвращает ли первая часть пространства имен? должен вернуть последнюю часть, а не первую
2

86

Я добавил substr в тест https://stackoverflow.com/a/25472778/2386943, и это самый быстрый способ тестирования (CentOS PHP 5.3.3, Ubuntu PHP 5.5.9) для i5.

$classNameWithNamespace=get_class($this);
return substr($classNameWithNamespace, strrpos($classNameWithNamespace, '\\')+1);

Полученные результаты

Reflection: 0.068084406852722 s ClassA
Basename: 0.12301609516144 s ClassA
Explode: 0.14073524475098 s ClassA
Substring: 0.059865570068359 s ClassA 

Код

namespace foo\bar\baz;
class ClassA{
  public function getClassExplode(){
    $c = array_pop(explode('\\', get_class($this)));
    return $c;
  }

  public function getClassReflection(){
    $c = (new \ReflectionClass($this))->getShortName();
    return $c;
  }

  public function getClassBasename(){
    $c = basename(str_replace('\\', '/', get_class($this)));
    return $c;
  }

  public function getClassSubstring(){
    $classNameWithNamespace = get_class($this);
    return substr($classNameWithNamespace, strrpos($classNameWithNamespace, '\\')+1);
  }
}

$a = new ClassA();
$num = 100000;

$rounds = 10;
$res = array(
    "Reflection" => array(),
    "Basename" => array(),
    "Explode" => array(),
    "Substring" => array()
);

for($r = 0; $r < $rounds; $r++){

  $start = microtime(true);
  for($i = 0; $i < $num; $i++){
    $a->getClassReflection();
  }
  $end = microtime(true);
  $res["Reflection"][] = ($end-$start);

  $start = microtime(true);
  for($i = 0; $i < $num; $i++){
    $a->getClassBasename();
  }
  $end = microtime(true);
  $res["Basename"][] = ($end-$start);

  $start = microtime(true);
  for($i = 0; $i < $num; $i++){
    $a->getClassExplode();
  }
  $end = microtime(true);
  $res["Explode"][] = ($end-$start);

  $start = microtime(true);
  for($i = 0; $i < $num; $i++){
    $a->getClassSubstring();
  }
  $end = microtime(true);
  $res["Substring"][] = ($end-$start);
}

echo "Reflection: ".array_sum($res["Reflection"])/count($res["Reflection"])." s ".$a->getClassReflection()."\n";
echo "Basename: ".array_sum($res["Basename"])/count($res["Basename"])." s ".$a->getClassBasename()."\n";
echo "Explode: ".array_sum($res["Explode"])/count($res["Explode"])." s ".$a->getClassExplode()."\n";
echo "Substring: ".array_sum($res["Substring"])/count($res["Substring"])." s ".$a->getClassSubstring()."\n";

== UPDATE ==

Как уже упоминалось в комментариях @MrBandersnatch, есть еще более быстрый способ сделать это:

return substr(strrchr(get_class($this), '\\'), 1);

Вот обновленные результаты теста с «SubstringStrChr» (экономит примерно до 0,001 с):

Reflection: 0.073065280914307 s ClassA
Basename: 0.12585079669952 s ClassA
Explode: 0.14593172073364 s ClassA
Substring: 0.060415267944336 s ClassA
SubstringStrChr: 0.059880912303925 s ClassA

5
Просто потому, что мы перечислили эффективность, я нашел это самым быстрым, сравнение с тестом, представленным в этом решении substr (strrchr (get_class ($ obj), '\\'), 1); Отражение: 0,084223914146423 с ClassA - Базовое имя: 0,13206427097321 s ClassA - Взрыв: 0,15331919193268 s ClassA - Подстрока: 0,068068099021912 s ClassA - Strrchar: 0,06472008228302 с ClassA -
ctatro85

Я только что наткнулся на эту ветку и добавил дополнительный тест для тестирования str_replace(__NAMESPACE__ . '\\', '', __CLASS__);. Результаты на слабой виртуальной машине показали, что она почти в два раза быстрее всех этих. php -f bench.php Reflection: 0.44037771224976 s ClassA Basename: 0.48089025020599 s ClassA Explode: 0.54955270290375 s ClassA Substring: 0.38200764656067 s ClassA Frank's Custom Benchmark: 0.22782742977142 s ClassA
Франклин П Струб

1
@MrBandersnatch вы правы. Я проверил ваше решение, и оно сэкономило мне около 0,001 с. Я обновил свой ответ с вашим!
МаБи

3
Предупреждение: этот код не работает с классами в глобальном пространстве имен (то есть: их полное имя равно их короткому имени)! Я совет тест что - то вроде: if ($pos = strrchr(static::class, '\\')) { .. } else { ... }.
Тристан Джахиер

1
Чтобы заставить его работать и в глобальном пространстве имен, просто добавьте к имени класса обратную косую черту :) - то есть:$classNameShort = substr(strrchr('\\' . get_class($this), '\\'), 1);
rosell.dk

26

Вот более простой способ сделать это, если вы используете Laravel PHP Framework:

<?php

// usage anywhere
// returns HelloWorld
$name = class_basename('Path\To\YourClass\HelloWorld');

// usage inside a class
// returns HelloWorld
$name = class_basename(__CLASS__);

8
Это не встроенная функция php, она выглядит как вспомогательная функция, предоставляемая laravel.
Стив Бузонас

7
Я думаю, что он сказал, что
Скотт

4
Спасибо, я использую Laravel, и этот ответ сэкономил мне кучу времени.
Джереми Уодхэмс


18

Я использую это:

basename(str_replace('\\', '/', get_class($object)));

Вы также можете попробовать: $ className = explode ('\\', basename (get_class ($ this))); $ className = array_pop ($ className); чтобы получить простое имя класса. Или используйте substr.
dompie

13
Работает только в Windows. В Windows слэш (/) и обратный слэш () используются в качестве разделителя каталогов. В других средах это косая черта (/) php.net/manual/en/function.basename.php
OzzyCzech

Я исправил это сейчас. Спасибо, @OzzyCzech.
Теодор Р. Смит

1
@OzzyCzech Я только столкнулся с этим при переходе с Windows на Ubuntu .... сводящий с ума. Завелось с помощью решения, упомянутого в обновлении MaBi.
Крис Бейкер

@OzzyCzech Почему работает только на Windows? вопрос касался полностью определенного имени пространства имен, если я не ошибаюсь и много лет назад, и пространства имен не зависят от ОС, и всегда с обратной косой чертой, как разделитель каталогов Windows.
FantomX1

16

Чтобы получить короткое имя как однострочное (начиная с PHP 5.4 ):

echo (new ReflectionClass($obj))->getShortName();

Это чистый подход и разумно быстро .


1
Интересно, как это сравнивается с извлечением строки в тестах? Кажется, это будет намного медленнее.
Неподтвержденный контакт

12

Я оказался в уникальной ситуации, когда ее instanceofнельзя было использовать (в частности, в чертах пространства имен), и мне нужно было короткое имя наиболее эффективным способом, поэтому я сделал небольшой собственный тест. Он включает в себя все различные методы и варианты ответов на этот вопрос.

$bench = new \xori\Benchmark(1000, 1000);     # https://github.com/Xorifelse/php-benchmark-closure
$shell = new \my\fancy\namespace\classname(); # Just an empty class named `classname` defined in the `\my\fancy\namespace\` namespace

$bench->register('strrpos', (function(){
    return substr(static::class, strrpos(static::class, '\\') + 1);
})->bindTo($shell));

$bench->register('safe strrpos', (function(){
    return substr(static::class, ($p = strrpos(static::class, '\\')) !== false ? $p + 1 : 0);
})->bindTo($shell));

$bench->register('strrchr', (function(){
    return substr(strrchr(static::class, '\\'), 1);
})->bindTo($shell));

$bench->register('reflection', (function(){
    return (new \ReflectionClass($this))->getShortName();
})->bindTo($shell));

$bench->register('reflection 2', (function($obj){
    return $obj->getShortName();
})->bindTo($shell), new \ReflectionClass($shell));

$bench->register('basename', (function(){
    return basename(str_replace('\\', '/', static::class));
})->bindTo($shell));

$bench->register('explode', (function(){
    $e = explode("\\", static::class);
    return end($e);
})->bindTo($shell));

$bench->register('slice', (function(){
    return join('',array_slice(explode('\\', static::class), -1));
})->bindTo($shell));    

print_r($bench->start());

Список полного результата здесь, но вот основные моменты:

  • Если вы собираетесь использовать отражение в любом случае, используя $obj->getShortName()самый быстрый метод , однако , используя только отражение для получения короткого имени, это почти самый медленный метод.
  • 'strrpos'может вернуть неправильное значение, если объект не находится в пространстве имен, поэтому пока 'safe strrpos'он немного медленнее, я бы сказал, что это победитель.
  • Для обеспечения 'basename'совместимости между Linux и Windows вам необходимо использовать str_replace()этот метод, который делает этот метод самым медленным из всех.

В упрощенной таблице результатов скорость измеряется по сравнению с самым медленным методом:

+-----------------+--------+
| registered name | speed  |
+-----------------+--------+
| reflection 2    | 70.75% |
| strrpos         | 60.38% |
| safe strrpos    | 57.69% |
| strrchr         | 54.88% |
| explode         | 46.60% |
| slice           | 37.02% |
| reflection      | 16.75% |
| basename        | 0.00%  |
+-----------------+--------+

8

Вы можете использовать explodeдля разделения пространства имен и endдля получения имени класса:

$ex = explode("\\", get_class($object));
$className = end($ex);

7

Yii путь

\yii\helpers\StringHelper::basename(get_class($model));

Yii использует этот метод в своем генераторе кода Gii

Методическая документация

Этот метод похож на функцию php basename () за исключением того, что он будет обрабатывать как \, так и / / как разделители каталогов, независимо от операционной системы. Этот метод был в основном создан для работы с пространствами имен php. При работе с реальными путями к файлам, php basename () должен нормально работать для вас. Примечание: этот метод не знает о фактической файловой системе или компонентах пути, таких как "..".

Больше информации:

https://github.com/yiisoft/yii2/blob/master/framework/helpers/BaseStringHelper.php http://www.yiiframework.com/doc-2.0/yii-helpers-basestringhelper.html#basename()-detail


Добро пожаловать в переполнение стека. Пожалуйста, предоставьте больше информации для вашего ответа. Что это делает и как можно это использовать.
Йенс

1
Это сработало для меня в Windows, но не в Linux, возможно, потому что пространства имен имеют форму обратной косой черты каталогов Windows '\', тогда как базовое имя linux рассматривает прямые косые черты разделителей каталогов '/'. Так что я обошел это с strtr. базовое имя (strtr ($ class, '\\', '/'))
FantomX1

6

Вот простое решение для PHP 5.4+

namespace {
    trait Names {
        public static function getNamespace() {
            return implode('\\', array_slice(explode('\\', get_called_class()), 0, -1));
        }

        public static function getBaseClassName() {
            return basename(str_replace('\\', '/', get_called_class()));
        }
    }
}

Что будет возвращаться?

namespace x\y\z {
    class SomeClass {
        use \Names;
    }

    echo \x\y\z\SomeClass::getNamespace() . PHP_EOL; // x\y\z
    echo \x\y\z\SomeClass::getBaseClassName() . PHP_EOL; // SomeClass
}

Расширенное имя класса и пространство имен работают хорошо для:

namespace d\e\f {

    class DifferentClass extends \x\y\z\SomeClass {

    }

    echo \d\e\f\DifferentClass::getNamespace() . PHP_EOL; // d\e\f
    echo \d\e\f\DifferentClass::getBaseClassName() . PHP_EOL; // DifferentClass
}

Как насчет класса в глобальном пространстве имен?

namespace {

    class ClassWithoutNamespace {
        use \Names;
    }

    echo ClassWithoutNamespace::getNamespace() . PHP_EOL; // empty string
    echo ClassWithoutNamespace::getBaseClassName() . PHP_EOL; // ClassWithoutNamespace
}

3

Если вам нужно знать имя класса, которое было вызвано из класса, и вам не нужно пространство имен, вы можете использовать это

$calledClass = get_called_class();
$name = strpos($calledClass, '\\') === false ?
    $calledClass : substr($calledClass, strrpos($calledClass, '\\') + 1);

Это замечательно, когда у вас есть метод внутри класса, который расширяется другими классами. Кроме того, это также работает, если пространства имен вообще не используются.

Пример:

<?php
namespace One\Two {
    class foo
    {
        public function foo()
        {
            $calledClass = get_called_class();
            $name = strpos($calledClass, '\\') === false ?
                $calledClass : substr($calledClass, strrpos($calledClass, '\\') + 1);

            var_dump($name);
        }
    }
}

namespace Three {
    class bar extends \One\Two\foo
    {
        public function bar()
        {
            $this->foo();
        }
    }
}

namespace {
    (new One\Two\foo)->foo();
    (new Three\bar)->bar();
}

// test.php:11:string 'foo' (length=3)
// test.php:11:string 'bar' (length=3)

2

Основываясь на ответе @MaBi, я сделал это:

trait ClassShortNameTrait
{
    public static function getClassShortName()
    {
        if ($pos = strrchr(static::class, '\\')) {
            return substr($pos, 1);
        } else {
            return static::class;
        }
    }
}

Что вы можете использовать так:

namespace Foo\Bar\Baz;

class A
{
    use ClassShortNameTrait;
}

A::classвозвращается Foo\Bar\Baz\A, но A::getClassShortName()возвращаетсяA .

Работает на PHP> = 5.5.


2

Я знаю, что это старый пост, но это то, что я использую - быстрее, чем все вышеперечисленное, просто вызовите этот метод из вашего класса, намного быстрее, чем с помощью Reflection

namespace Foo\Bar\Baz;

class Test {
    public function getClass() {
        return str_replace(__NAMESPACE__.'\\', '', static::class);
    }
}

К сожалению, это работает только в том случае, если вы вызываете его в классе, имя которого вы хотите, а не только для любого имени класса в виде строки.
Юрчик

1

Нашел на странице документации get_class , где он был размещен мной на nwhiting dot com .

function get_class_name($object = null)
{
    if (!is_object($object) && !is_string($object)) {
        return false;
    }

    $class = explode('\\', (is_string($object) ? $object : get_class($object)));
    return $class[count($class) - 1];
}

Но идея пространств имен состоит в том, чтобы структурировать ваш код. Это также означает, что вы можете иметь классы с одним и тем же именем в нескольких пространствах имен. Таким образом, теоретически объект, который вы передаете, может иметь имя класса (обрезанное), но при этом он будет совершенно другим объектом, чем вы ожидаете.

Кроме того, вы можете проверить наличие определенного базового класса , и в этом случае get_classничего не получится. Вы можете проверить оператора instanceof.


1

Вы можете получить неожиданный результат, когда у класса нет пространства имен. Т.е. get_classвозвращается Foo, тогда $baseClassбы oo.

$baseClass = substr(strrchr(get_class($this), '\\'), 1);

Это может быть легко исправлено путем добавления префикса get_classс обратной косой чертой:

$baseClass = substr(strrchr('\\'.get_class($this), '\\'), 1);

Теперь также классы без пространства имен будут возвращать правильное значение.


1

Старый добрый regex кажется быстрее, чем большинство предыдущих показанных методов:

// both of the below calls will output: ShortClassName

echo preg_replace('/.*\\\\/', '', 'ShortClassName');
echo preg_replace('/.*\\\\/', '', 'SomeNamespace\SomePath\ShortClassName');

Так что это работает, даже если вы предоставляете краткое имя класса или полностью определенное (каноническое) имя класса.

Регулярное выражение использует все предыдущие символы до тех пор, пока не будет найден последний разделитель (который также используется). Таким образом, оставшаяся строка будет кратким именем класса.

Если вы хотите использовать другой разделитель (например, /), просто используйте вместо этого этот разделитель. Не забудьте избежать обратной косой черты (т. Е. \), А также шаблона char (т. Е. /) Во входном шаблоне.


1

Поскольку «ReflectionClass» может зависеть от версии, просто используйте следующее:

if(class_basename(get_class($object)) == 'Name') {
... do this ...
}

или даже очистить

if(class_basename(ClassName::class) == 'ClassName') {
... do this ...
}

0

Цитирую php.net:

В Windows слэш (/) и обратный слэш () используются в качестве символа разделителя каталогов. В других средах это косая черта (/).

Исходя из этой информации и расширения от ответа arzzzen, это должно работать как в системах Windows, так и в системах Nix *:

<?php

if (basename(str_replace('\\', '/', get_class($object))) == 'Name') {
    // ... do this ...
}

Примечание. Я провел тестирование ReflectionClassс basename+str_replace+get_classиспользованием отражения и примерно на 20% быстрее, чем при использовании подхода с базовым именем, но с YMMV.


0

Самое быстрое и простое решение, которое работает в любой среде:

<?php

namespace \My\Awesome\Namespace;

class Foo {

  private $shortName;

  public function fastShortName() {
    if ($this->shortName === null) {
      $this->shortName = explode("\\", static::class);
      $this->shortName = end($this->shortName);
    }
    return $this->shortName;
  }

  public function shortName() {
    return basename(strtr(static::class, "\\", "/"));
  }

}

echo (new Foo())->shortName(); // "Foo"

?>

1
Вот почему я хотел бы, чтобы в PHP были внутренние операторы информации о классе. Использование внешнего рефлектора для выполнения того, что должно быть настолько простым, насколько $Object->__class->getShortName()меня бесит PHP. Ваш подход работает, но теперь вы добавляете конкретные методы в ваши классы, просто чтобы показать, что должно быть языковой конструкцией.
AgmLauncher

PHP без «конкретных» (или мы должны называть их процедурными) функций невозможен. Давайте подождем PHP 6 (ну, если он когда-нибудь придет).
Fleshgrinder


0

Если вы просто удаляете пространства имен и хотите что-нибудь после последнего \ в имени класса с пространством имен (или просто именем, если нет '\'), вы можете сделать что-то вроде этого:

$base_class = preg_replace('/^([\w\\\\]+\\\\)?([^\\\\]+)$/', '$2', get_class($myobject));

По сути, это регулярное выражение для получения любой комбинации символов или обратной косой черты вверх и до последнего обратного слеша, чтобы возвращать только символы без обратной косой черты вверх и до конца строки. Добавление? после первой группировки означает, что совпадение с образцом не существует, оно просто возвращает полную строку.


Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.