การจัดการความสัมพันธ์ใน Laravel โดยยึดตามรูปแบบพื้นที่เก็บข้อมูล


120

ขณะสร้างแอปใน Laravel 4 หลังจากอ่านหนังสือของ T. Otwell เกี่ยวกับรูปแบบการออกแบบที่ดีใน Laravel ฉันพบว่าตัวเองกำลังสร้างที่เก็บสำหรับทุกตารางบนแอปพลิเคชัน

ฉันลงเอยด้วยโครงสร้างตารางต่อไปนี้:

  • นักเรียน: id, name
  • หลักสูตร: id, name, teacher_id
  • ครู: id, name
  • Assignments: id, name, course_id
  • คะแนน (ทำหน้าที่เป็นตัวหมุนระหว่างนักเรียนและงาน): student_id, assignment_id, คะแนน

ฉันมีคลาสที่เก็บที่มีวิธีการค้นหาสร้างอัปเดตและลบสำหรับตารางเหล่านี้ทั้งหมด แต่ละที่เก็บมีแบบจำลองที่มีประสิทธิภาพซึ่งโต้ตอบกับฐานข้อมูล ความสัมพันธ์ที่กำหนดไว้ในรูปแบบเอกสารต่อ Laravel ของ: http://laravel.com/docs/eloquent#relationships

เมื่อสร้างหลักสูตรใหม่สิ่งที่ฉันทำคือเรียกใช้เมธอด create บน Course Repository หลักสูตรนั้นมีงานมอบหมายดังนั้นเมื่อสร้างหลักสูตรฉันจึงต้องการสร้างรายการในตารางคะแนนสำหรับนักเรียนแต่ละคนในหลักสูตรด้วย ฉันทำสิ่งนี้ผ่าน Assignment Repository ซึ่งหมายความว่าที่เก็บงานมอบหมายจะสื่อสารกับโมเดล Eloquent สองแบบโดยใช้โมเดล Assignment และ Student

คำถามของฉันคือเนื่องจากแอปนี้อาจมีขนาดใหญ่ขึ้นและจะมีการแนะนำความสัมพันธ์มากขึ้นการสื่อสารกับโมเดล Eloquent ที่แตกต่างกันในที่เก็บนั้นดีหรือไม่หรือควรทำโดยใช้ที่เก็บอื่นแทน (ฉันหมายถึงการเรียกที่เก็บอื่นจากที่เก็บ Assignment ) หรือควรทำในรุ่น Eloquent ด้วยกันทั้งหมด?

นอกจากนี้การใช้ตารางคะแนนเป็นจุดหมุนระหว่างงานกับนักเรียนเป็นแนวทางปฏิบัติที่ดีหรือควรทำที่อื่น

คำตอบ:


71

โปรดทราบว่าคุณกำลังขอความคิดเห็น: D

นี่ของฉัน:

TL; DR: ใช่ไม่เป็นไร

คุณสบายดี!

ฉันทำในสิ่งที่คุณทำบ่อยๆและพบว่ามันได้ผลดี

อย่างไรก็ตามฉันมักจะจัดระเบียบที่เก็บเกี่ยวกับตรรกะทางธุรกิจแทนที่จะมี repo-per-table สิ่งนี้มีประโยชน์เนื่องจากเป็นมุมมองที่เน้นว่าแอปพลิเคชันของคุณควรแก้ปัญหา "ธุรกิจ" ของคุณอย่างไร

หลักสูตรคือ "เอนทิตี" ที่มีแอตทริบิวต์ (ชื่อรหัส ฯลฯ ) และแม้แต่เอนทิตีอื่น ๆ (Assignments ซึ่งมีแอตทริบิวต์ของตนเองและอาจเป็นเอนทิตี)

ที่เก็บ "หลักสูตร" ของคุณควรสามารถส่งคืนหลักสูตรและแอตทริบิวต์ / การมอบหมายของหลักสูตร (รวมถึงการมอบหมายงาน)

คุณสามารถทำสิ่งนั้นให้สำเร็จได้ด้วย Eloquent โชคดี

(ฉันมักจะลงเอยด้วยที่เก็บต่อตาราง แต่ที่เก็บบางแห่งจะใช้มากกว่าที่เก็บอื่น ๆ และมีวิธีการอื่น ๆ อีกมากมายที่เก็บ "หลักสูตร" ของคุณอาจมีคุณสมบัติครบถ้วนมากกว่าที่เก็บ Assignments ของคุณมากตัวอย่างเช่นหากคุณ แอปพลิเคชันมีศูนย์เพิ่มเติมเกี่ยวกับหลักสูตรและคอลเลกชันของ Assignments ของหลักสูตรน้อยลง)

ส่วนที่ยุ่งยาก

ฉันมักจะใช้ที่เก็บภายในที่เก็บของฉันเพื่อดำเนินการกับฐานข้อมูลบางอย่าง

ที่เก็บใด ๆ ที่ใช้ Eloquent เพื่อจัดการกับข้อมูลจะส่งคืนโมเดล Eloquent ในแง่นี้จะเป็นการดีหากโมเดลหลักสูตรของคุณใช้ความสัมพันธ์ในตัวเพื่อดึงหรือบันทึก Assignments (หรือกรณีการใช้งานอื่น ๆ ) "การนำไปใช้" ของเราถูกสร้างขึ้นโดยใช้ Eloquent

จากมุมมองในทางปฏิบัติสิ่งนี้สมเหตุสมผล เราไม่น่าจะเปลี่ยนแหล่งข้อมูลเป็นสิ่งที่ Eloquent ไม่สามารถจัดการได้ (เป็นแหล่งข้อมูลที่ไม่ใช่ sql)

ORMS

ส่วนที่ยากที่สุดของการตั้งค่านี้สำหรับฉันอย่างน้อยก็คือการพิจารณาว่า Eloquent ช่วยเหลือหรือทำร้ายเราจริงหรือไม่ ORM เป็นเรื่องที่ยุ่งยากเพราะแม้ว่าจะช่วยเราได้มากจากมุมมองที่ใช้งานได้จริง แต่ก็ยังจับคู่โค้ด "เอนทิตีตรรกะทางธุรกิจ" ของคุณกับโค้ดที่ทำการดึงข้อมูล

ประเภทนี้ทำให้สับสนว่าความรับผิดชอบของพื้นที่เก็บข้อมูลของคุณคือการจัดการข้อมูลหรือจัดการการดึง / อัปเดตเอนทิตี (เอนทิตีโดเมนธุรกิจ)

นอกจากนี้ยังทำหน้าที่เป็นวัตถุที่คุณส่งผ่านไปยังมุมมองของคุณ หากคุณต้องหลีกหนีจากการใช้โมเดล Eloquent ในที่เก็บในภายหลังคุณจะต้องตรวจสอบให้แน่ใจว่าตัวแปรที่ส่งไปยังมุมมองของคุณทำงานในลักษณะเดียวกันหรือมีวิธีการเดียวกันที่พร้อมใช้งานมิฉะนั้นการเปลี่ยนแหล่งข้อมูลของคุณจะเปลี่ยนเป็นการเปลี่ยน มุมมองและคุณ (บางส่วน) สูญเสียจุดประสงค์ในการสรุปตรรกะของคุณไปยังที่เก็บตั้งแต่แรก - ความสามารถในการบำรุงรักษาของโครงการของคุณจะลดลงตาม

อย่างไรก็ตามสิ่งเหล่านี้เป็นความคิดที่ไม่สมบูรณ์ ตามที่ระบุไว้เป็นเพียงความคิดเห็นของฉันซึ่งเกิดขึ้นจากการอ่านการออกแบบโดเมนที่ขับเคลื่อนด้วยและดูวิดีโอเช่นคำปราศรัยของ "ลุงบ๊อบ"ที่ Ruby Midwest ภายในปีที่แล้ว


1
ในความคิดของคุณจะเป็นทางเลือกที่ดีหรือไม่หากที่เก็บส่งคืนออบเจ็กต์การถ่ายโอนข้อมูลแทนที่จะเป็นวัตถุที่คมคาย แน่นอนว่านี่จะบ่งบอกถึงการแปลงเพิ่มเติมจากการพูดเก่งเป็น dto แต่อย่างน้อยวิธีนี้คุณแยกคอนโทรลเลอร์ / มุมมองของคุณออกจากการใช้ orm ปัจจุบัน
federivo

1
ฉันได้ทดลองกับตัวเองเล็กน้อยและพบว่ามันเล็กน้อยในด้านที่ทำไม่ได้ ที่กล่าวมาฉันชอบความคิดนั้นในนามธรรม อย่างไรก็ตามวัตถุคอลเลกชันฐานข้อมูลของ Illuminate ทำหน้าที่เหมือนกับอาร์เรย์และวัตถุ Model ทำหน้าที่เหมือนกับวัตถุ StdClass เพียงพอที่จะทำให้เราสามารถพูดติดกับ Eloquent ได้จริงและยังคงใช้อาร์เรย์ / วัตถุในอนาคตหากเราจำเป็นต้องใช้
fideloper

4
@fideloper ฉันรู้สึกว่าถ้าฉันใช้ที่เก็บข้อมูลฉันจะสูญเสียความสวยงามทั้งหมดของ ORM ที่ Eloquent มอบให้ เมื่อดึงอ็อบเจ็กต์บัญชีผ่านเมธอดที่เก็บของ$a = $this->account->getById(1)ฉันฉันไม่สามารถเชื่อมโยงเมธอดเช่น$a->getActiveUsers(). โอเคฉันสามารถใช้ได้$a->users->...แต่แล้วฉันก็ส่งคืนคอลเลคชัน Eloquent และไม่มีวัตถุ stdClass และเชื่อมโยงกับ Eloquent อีกครั้ง วิธีแก้ปัญหานี้คืออะไร? การประกาศวิธีอื่นในที่เก็บผู้ใช้เช่น $user->getActiveUsersByAccount($a->id);? ชอบที่จะได้ยินว่าคุณแก้ปัญหานี้อย่างไร ...
santacruz

1
ORM นั้นแย่มากสำหรับสถาปัตยกรรมระดับ Enterprise (ish) เพราะทำให้เกิดปัญหาเช่นนี้ ท้ายที่สุดคุณต้องตัดสินใจว่าอะไรที่เหมาะสมที่สุดสำหรับการสมัครของคุณ โดยส่วนตัวเมื่อใช้ที่เก็บกับ Eloquent (90% ของเวลา!) ฉันใช้ Eloquent และพยายามอย่างเต็มที่ในการปฏิบัติต่อโมเดลและคอลเลกชันเช่น stdClasses & Arrays (เพราะคุณทำได้!) ดังนั้นหากฉันต้องการเปลี่ยนไปใช้อย่างอื่นก็เป็นไปได้
fideloper

5
ไปข้างหน้าและใช้โมเดลขี้เกียจโหลด คุณสามารถทำให้แบบจำลองโดเมนจริงทำงานเช่นนั้นได้หากคุณไม่ใช้ Eloquent แต่อย่างจริงจังคุณจะเปลี่ยนไปใช้ Eloquent หรือไม่? ในเงินหนึ่งปอนด์! (อย่าลงน้ำโดยพยายามยึดติดกับ "กฎ"! ฉันทำลายทั้งหมดของฉันตลอดเวลา)
fideloper

224

ฉันกำลังทำโปรเจ็กต์ขนาดใหญ่โดยใช้ Laravel 4 และต้องตอบคำถามทั้งหมดที่คุณถามตอนนี้ หลังจากอ่านหนังสือ Laravel ที่มีอยู่ทั้งหมดที่ Leanpub และ Googling มากมายฉันก็คิดโครงสร้างต่อไปนี้

  1. หนึ่งคลาส Eloquent Model ต่อตารางที่สามารถระบุได้
  2. คลาส Repository หนึ่งคลาสต่อ Eloquent Model
  3. คลาสเซอร์วิสที่อาจสื่อสารระหว่างคลาส Repository หลายคลาส

สมมติว่าฉันกำลังสร้างฐานข้อมูลภาพยนตร์ ฉันจะมีคลาส Eloquent Model ต่อไปนี้เป็นอย่างน้อย:

  • หนัง
  • สตูดิโอ
  • ผู้อำนวยการ
  • นักแสดงชาย
  • ทบทวน

คลาสที่เก็บจะห่อหุ้มคลาส Eloquent Model แต่ละคลาสและรับผิดชอบการดำเนินการ CRUD บนฐานข้อมูล คลาสที่เก็บอาจมีลักษณะดังนี้:

  • MovieRepository
  • StudioRepository
  • DirectorRepository
  • ActorRepository
  • ReviewRepository

แต่ละคลาสที่เก็บจะขยายคลาส BaseRepository ซึ่งใช้อินเตอร์เฟสต่อไปนี้:

interface BaseRepositoryInterface
{
    public function errors();

    public function all(array $related = null);

    public function get($id, array $related = null);

    public function getWhere($column, $value, array $related = null);

    public function getRecent($limit, array $related = null);

    public function create(array $data);

    public function update(array $data);

    public function delete($id);

    public function deleteWhere($column, $value);
}

คลาสบริการใช้เพื่อรวมที่เก็บข้อมูลหลายแห่งเข้าด้วยกันและมี "ตรรกะทางธุรกิจ" ที่แท้จริงของแอปพลิเคชัน ตัวควบคุมจะสื่อสารกับคลาสบริการสำหรับการสร้างอัปเดตและลบเท่านั้น

ดังนั้นเมื่อฉันต้องการสร้าง Movie record ใหม่ในฐานข้อมูลคลาส MovieController ของฉันอาจมีวิธีการดังต่อไปนี้:

public function __construct(MovieRepositoryInterface $movieRepository, MovieServiceInterface $movieService)
{
    $this->movieRepository = $movieRepository;
    $this->movieService = $movieService;
}

public function postCreate()
{
    if( ! $this->movieService->create(Input::all()))
    {
        return Redirect::back()->withErrors($this->movieService->errors())->withInput();
    }

    // New movie was saved successfully. Do whatever you need to do here.
}

ขึ้นอยู่กับคุณที่จะกำหนดวิธีการโพสต์ข้อมูลไปยังคอนโทรลเลอร์ของคุณ แต่สมมติว่าข้อมูลที่ส่งคืนโดย Input :: all () ในเมธอด postCreate () จะมีลักษณะดังนี้:

$data = array(
    'movie' => array(
        'title'    => 'Iron Eagle',
        'year'     => '1986',
        'synopsis' => 'When Doug\'s father, an Air Force Pilot, is shot down by MiGs belonging to a radical Middle Eastern state, no one seems able to get him out. Doug finds Chappy, an Air Force Colonel who is intrigued by the idea of sending in two fighters piloted by himself and Doug to rescue Doug\'s father after bombing the MiG base.'
    ),
    'actors' => array(
        0 => 'Louis Gossett Jr.',
        1 => 'Jason Gedrick',
        2 => 'Larry B. Scott'
    ),
    'director' => 'Sidney J. Furie',
    'studio' => 'TriStar Pictures'
)

เนื่องจาก MovieRepository ไม่ควรรู้วิธีสร้างเร็กคอร์ด Actor, Director หรือ Studio ในฐานข้อมูลเราจึงใช้คลาส MovieService ของเราซึ่งอาจมีลักษณะดังนี้:

public function __construct(MovieRepositoryInterface $movieRepository, ActorRepositoryInterface $actorRepository, DirectorRepositoryInterface $directorRepository, StudioRepositoryInterface $studioRepository)
{
    $this->movieRepository = $movieRepository;
    $this->actorRepository = $actorRepository;
    $this->directorRepository = $directorRepository;
    $this->studioRepository = $studioRepository;
}

public function create(array $input)
{
    $movieData    = $input['movie'];
    $actorsData   = $input['actors'];
    $directorData = $input['director'];
    $studioData   = $input['studio'];

    // In a more complete example you would probably want to implement database transactions and perform input validation using the Laravel Validator class here.

    // Create the new movie record
    $movie = $this->movieRepository->create($movieData);

    // Create the new actor records and associate them with the movie record
    foreach($actors as $actor)
    {
        $actorModel = $this->actorRepository->create($actor);
        $movie->actors()->save($actorModel);
    }

    // Create the director record and associate it with the movie record
    $director = $this->directorRepository->create($directorData);
    $director->movies()->associate($movie);

    // Create the studio record and associate it with the movie record
    $studio = $this->studioRepository->create($studioData);
    $studio->movies()->associate($movie);

    // Assume everything worked. In the real world you'll need to implement checks.
    return true;
}

ดังนั้นสิ่งที่เราเหลืออยู่คือการแยกความกังวลที่ดีและสมเหตุสมผล ที่เก็บจะทราบเฉพาะโมเดล Eloquent ที่แทรกและดึงข้อมูลจากฐานข้อมูล ตัวควบคุมไม่สนใจที่เก็บข้อมูลพวกเขาเพียงแค่ส่งต่อข้อมูลที่รวบรวมจากผู้ใช้และส่งต่อไปยังบริการที่เหมาะสม บริการไม่สนใจว่าข้อมูลที่ได้รับจะบันทึกลงในฐานข้อมูลอย่างไรเพียงส่งต่อข้อมูลที่เกี่ยวข้องที่ผู้ควบคุมมอบให้ไปยังที่เก็บที่เหมาะสม


8
ความคิดเห็นนี้เป็นแนวทางที่สะอาดกว่าปรับขนาดได้และบำรุงรักษาได้ดีกว่า
Andreas

4
+1! ที่จะช่วยฉันได้มากขอบคุณสำหรับการแบ่งปันกับเรา! สงสัยว่าคุณจัดการตรวจสอบสิ่งต่าง ๆ ภายในบริการได้อย่างไรถ้าเป็นไปได้คุณช่วยอธิบายสั้น ๆ ว่าคุณทำอะไรได้บ้าง? ขอบคุณต่อไป! :)
Paulo Freitas

6
เช่นเดียวกับที่ @PauloFreitas กล่าวว่ามันน่าสนใจที่จะดูว่าคุณจัดการกับส่วนการตรวจสอบความถูกต้องอย่างไรและฉันก็สนใจในส่วนของข้อยกเว้นเช่นกัน (คุณใช้ข้อยกเว้นเหตุการณ์หรือคุณจัดการสิ่งนี้ตามที่คุณแนะนำใน ตัวควบคุมผ่านการส่งคืนบูลีนในบริการของคุณ?) ขอบคุณ!
Nicolas

11
เขียนได้ดีแม้ว่าฉันจะไม่แน่ใจว่าทำไมคุณถึงฉีด movieRepository ลงใน MovieController เนื่องจากคอนโทรลเลอร์ไม่ควรทำอะไรโดยตรงกับที่เก็บหรือไม่ใช่วิธี postCreate ของคุณโดยใช้ movieRepository ดังนั้นฉันถือว่าคุณทิ้งไว้โดยไม่ได้ตั้งใจ ?
davidnknight

15
คำถามเกี่ยวกับเรื่องนี้ทำไมคุณถึงใช้ที่เก็บในตัวอย่างนี้ นี่เป็นคำถามที่ตรงไปตรงมาสำหรับฉันดูเหมือนว่าคุณกำลังใช้ที่เก็บ แต่อย่างน้อยในตัวอย่างนี้ที่เก็บไม่ได้ทำอะไรเลยนอกจากให้อินเทอร์เฟซเดียวกับ Eloquent และท้ายที่สุดคุณยังคงผูกติดอยู่กับ Eloquent เพราะ คลาสบริการของคุณใช้ฝีปากโดยตรงในนั้น ( $studio->movies()->associate($movie);)
Kevin Mitchell

5

ฉันชอบที่จะคิดในแง่ของสิ่งที่โค้ดของฉันกำลังทำและสิ่งที่ต้องรับผิดชอบมากกว่า "ถูกหรือผิด" นี่คือวิธีที่ฉันแยกความรับผิดชอบออกจากกัน:

  • ตัวควบคุมคือเลเยอร์ HTTP และการร้องขอเส้นทางผ่านไปยัง apis ที่อยู่ภายใต้ (aka มันควบคุมการไหล)
  • โมเดลแสดงถึงสคีมาฐานข้อมูลและบอกแอปพลิเคชันว่าข้อมูลมีลักษณะอย่างไรความสัมพันธ์ที่อาจมีตลอดจนแอตทริบิวต์ส่วนกลางที่อาจจำเป็น (เช่นเมธอดชื่อสำหรับส่งคืนชื่อและนามสกุลที่ต่อกัน)
  • ที่เก็บแสดงถึงการสืบค้นและการโต้ตอบที่ซับซ้อนมากขึ้นกับโมเดล (ฉันไม่ได้ทำแบบสอบถามใด ๆ เกี่ยวกับวิธีการแบบจำลอง)
  • เครื่องมือค้นหา - คลาสที่ช่วยฉันสร้างคำค้นหาที่ซับซ้อน

ด้วยเหตุนี้จึงเป็นเรื่องที่สมเหตุสมผลทุกครั้งที่จะใช้ที่เก็บ (ไม่ว่าคุณจะสร้าง interface.etc. เป็นหัวข้ออื่นทั้งหมด) ฉันชอบแนวทางนี้เพราะนั่นหมายความว่าฉันรู้ว่าจะต้องไปที่ไหนเมื่อต้องทำงานบางอย่าง

ฉันมักจะสร้างที่เก็บฐานซึ่งโดยปกติจะเป็นคลาสนามธรรมซึ่งกำหนดค่าเริ่มต้นหลัก - โดยพื้นฐานแล้วการดำเนินการ CRUD จากนั้นเด็กแต่ละคนสามารถขยายและเพิ่มวิธีการได้ตามความจำเป็นหรือเกินค่าเริ่มต้น การใส่โมเดลของคุณยังช่วยให้รูปแบบนี้มีประสิทธิภาพมาก


คุณสามารถแสดงการใช้งาน BaseRepository ของคุณได้หรือไม่? ฉันก็ทำแบบนี้เหมือนกันและฉันก็สงสัยว่าคุณทำอะไร
Odyssee

คิดว่า getById, getByName, getByTitle, บันทึกประเภท method.etc - โดยทั่วไปวิธีการที่ใช้กับที่เก็บทั้งหมดภายในโดเมนต่างๆ
Oddman

5

คิดว่า Repositories เป็นตู้เก็บข้อมูลที่สอดคล้องกัน (ไม่ใช่แค่ ORM ของคุณ) แนวคิดคือคุณต้องการดึงข้อมูลใน API ที่ใช้งานง่ายที่สอดคล้องกัน

หากคุณพบว่าตัวเองกำลังทำ Model :: all (), Model :: find (), Model :: create () คุณอาจไม่ได้รับประโยชน์มากนักจากการแยกที่เก็บข้อมูลออกจากนามธรรม ในทางกลับกันหากคุณต้องการใช้ตรรกะทางธุรกิจเพิ่มขึ้นอีกเล็กน้อยในการสืบค้นหรือการดำเนินการของคุณคุณอาจต้องการสร้างที่เก็บเพื่อให้ใช้ API ได้ง่ายขึ้นในการจัดการกับข้อมูล

ฉันคิดว่าคุณกำลังถามว่าที่เก็บจะเป็นวิธีที่ดีที่สุดในการจัดการกับไวยากรณ์ verbose ที่จำเป็นในการเชื่อมต่อโมเดลที่เกี่ยวข้องหรือไม่ มีบางสิ่งที่ฉันสามารถทำได้ทั้งนี้ขึ้นอยู่กับสถานการณ์:

  1. การแขวนโมเดลลูกใหม่ออกจากโมเดลแม่ (หนึ่งต่อหนึ่งหรือหลายตัว) ฉันจะเพิ่มวิธีการลงในที่เก็บลูกบางอย่างเช่นcreateWithParent($attributes, $parentModelInstance)นี้และจะเพิ่ม$parentModelInstance->idลงในparent_idฟิลด์ของแอตทริบิวต์และเรียกสร้าง

  2. การแนบความสัมพันธ์แบบหลายกลุ่มฉันสร้างฟังก์ชันในแบบจำลองเพื่อให้สามารถเรียกใช้ $ instance-> attachChild ($ childInstance) โปรดทราบว่าสิ่งนี้ต้องการองค์ประกอบที่มีอยู่ทั้งสองด้าน

  3. การสร้างโมเดลที่เกี่ยวข้องในการรันครั้งเดียวฉันสร้างสิ่งที่ฉันเรียกว่าเกตเวย์ (อาจจะผิดไปจากคำจำกัดความของฟาวเลอร์) วิธีที่ฉันสามารถเรียก $ gateway-> createParentAndChild ($ parentAttributes, $ childAttributes) แทนตรรกะที่อาจเปลี่ยนแปลงหรือจะทำให้ตรรกะที่ฉันมีในคอนโทรลเลอร์หรือคำสั่งซับซ้อนขึ้น

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.